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
19 changes: 19 additions & 0 deletions cli/deferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Deferred<T = void> {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (error: Error) => void;
}

export function defer<T>(): Deferred<T> {
let resolve: Deferred<T>["resolve"] | undefined = void 0;
let reject: Deferred<T>["reject"] | undefined = void 0;
let promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
if (!resolve || !reject) {
throw new Error("unable to create Deferred!");
} else {
return { resolve, reject, promise };
}
}
32 changes: 32 additions & 0 deletions cli/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Operation, Task } from "../deps.ts";
import { action, run, spawn, suspend } from "../deps.ts";
import { defer } from "./deferred.ts";

export function main(op: (args: string[]) => Operation<void>): Task<void> {
return run(function* Main() {
let done = defer<number>();

yield* spawn(function* () {
try {
yield* op(Deno.args);
done.resolve(0);
} catch (error) {
console.error(String(error));
done.resolve(1);
}
});

let code = yield* action<number>(function* (resolve) {
done.promise.then(resolve);
let interrupt = () => resolve(1);
try {
Deno.addSignalListener("SIGINT", interrupt);
yield* suspend();
} finally {
Deno.removeSignalListener("SIGINT", interrupt);
}
});

Deno.exit(code);
});
}
25 changes: 25 additions & 0 deletions cli/pls-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Route } from "./router.ts";

import VERSION from "../version.json" assert { type: "json" };

export const PlsCommand: Route = {
options: [
{
type: "boolean",
name: "version",
alias: "V",
description: "Print version information",
},
],
help: {
HEAD: `pls ${VERSION}`,
USAGE: "pls [OPTIONS] COMMAND",
},
*handle({ flags, children }) {
if (flags.version) {
console.log(VERSION);
} else {
yield* children;
}
},
};
12 changes: 12 additions & 0 deletions cli/pls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { main } from "./main.ts";
import { dispatch } from "./router.ts";
import { PlsCommand } from "./pls-command.ts";
import { RunCommand } from "./run-command.ts";

await main(function* (args) {
yield* dispatch(["pls", ...args], {
"pls": [PlsCommand, {
"run :MODULE": RunCommand,
}],
});
});
147 changes: 147 additions & 0 deletions cli/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Operation } from "../deps.ts";

import {
parse as parseFlags,
ParseOptions,
} from "https://deno.land/std@0.159.0/flags/mod.ts";

export function* dispatch(
args: string[],
routes: Routes,
matched: string[] = [],
): Operation<void> {
let shellwords = parseFlags(args)._;
match:
for (let [key, value] of Object.entries(routes)) {
let pattern: PatternElement[] = key.split(/\s/).map((name) => {
if (name.startsWith(":")) {
return { type: "dynamic", required: true, name: name.slice(1) };
} else {
return { type: "static", value: name };
}
});
let segments: Record<string, string> = {};
let rest = args.slice();
let copy = shellwords.slice();
for (let element of pattern) {
let top = copy.shift();

if (element.type === "dynamic") {
if (top) {
segments[element.name] = String(top);
rest.splice(rest.indexOf(String(top), 1));
matched = matched.concat(element.name);
}
} else if (element.value !== top) {
continue match;
} else {
rest.splice(rest.indexOf(element.value), 1);
matched = matched.concat(element.value);
}
}
let [handler, subroutes] = Array.isArray(value) ? value : [value];

let helpful = helpify(handler);

let parseOptions = routeOptions(helpful);

let flags = parseFlags(rest, parseOptions);

let children = subroutes
? dispatch(flags._.map(String), subroutes, matched)
: { *[Symbol.iterator]() {} };

let props: RouteProps = {
flags,
route: helpful,
children,
segments,
};

return yield* helpful.handle(props);
}
}

function routeOptions(route: Route): ParseOptions {
return route.options.reduce((options, spec) => {
options[spec.type].push(spec.name);
if (spec.alias) {
options.alias[spec.name] = spec.alias;
}
return options;
}, {
boolean: [] as string[],
number: [] as string[],
string: [] as string[],
alias: {} as Record<string, string>,
stopEarly: true,
});
}

function helpify(route: Route): Route {
let options = route.options.slice();
options.splice(0, 0, {
type: "boolean",
name: "help",
alias: "h",
description: "Print help information",
});
return {
...route,
options,
*handle(props) {
if (props.flags.help) {
printHelp(props.route);
} else {
yield* route.handle(props);
}
},
};
}

export type PatternElement = {
type: "dynamic";
name: string;
required: boolean;
} | {
type: "static";
value: string;
};

export interface RouteProps<TFlags = unknown> {
flags: TFlags;
route: Route;
segments: Record<string, string>;
children: Operation<void>;
}

export interface Route {
options: {
type: "boolean" | "string" | "number";
name: string;
alias?: string;
description: string;
}[];
help: {
HEAD: string;
USAGE: string;
};
handle(props: RouteProps): Operation<void>;
}

export type Routes = Record<string, Route | [Route, Routes]>;

function printHelp(route: Route): void {
let optionsTable = route.options.map((opt) => {
let line = new Array(80).fill(" ", 0, 79);
if (opt.alias) {
line.splice(4, 3, ...`-${opt.alias},`);
}
line.splice(8, opt.name.length + 2, ...`--${opt.name}`);
line.splice(20, opt.description.length, ...opt.description);
return line.join("");
}).join("\n");
console.log(`${route.help.HEAD}\n`);
console.log(`USAGE:\n ${route.help.USAGE}\n`);
console.log(`OPTIONS:\n${optionsTable}`);
}
45 changes: 45 additions & 0 deletions cli/run-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Route } from "./router.ts";
import { load, print } from "../mod.ts";
import { LogContext, resolve, spawn } from "../deps.ts";

export const RunCommand: Route = {
options: [],
help: {
HEAD: "Evaluate a PlatformScript program",
USAGE: "pls run [OPTIONS] MODULE",
},
*handle({ segments, route }) {
if (!segments.MODULE) {
console.error(`USAGE: ${route.help.USAGE}
missing required argument MODULE`);
return;
}

let address = segments.MODULE;

let location = isURL(address) ? address : `file://${resolve(address)}`;
// setup logging context to output to stdout.
yield* spawn(function* () {
let logs = yield* LogContext;
let i = yield* logs.output;
for (let next = yield* i; !next.done; next = yield* i) {
console.log(next.value.message);
}
});
// load the module
let mod = yield* load({ location });

// print its value
let str = print(mod.value);
console.log(str.value);
},
};

function isURL(value: string): boolean {
try {
new URL(value);
return true;
} catch (_) {
return false;
}
}
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"tasks": {
"test": "deno test --allow-read --allow-net",
"pls": "deno run -A cli/pls.ts",
"test": "deno test --allow-read --allow-net --allow-run",
"build:npm": "deno run -A tasks/build-npm.ts",
"changelog-entry": "deno run -A tasks/changelog-entry.ts",

Expand Down
22 changes: 0 additions & 22 deletions pls.ts

This file was deleted.

40 changes: 40 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "./suite.ts";
import VERSION from "../version.json" assert { type: "json" };

describe("pls", () => {
it("can be invoked for help", async () => {
expect(await exec("-h")).toContain(VERSION);
expect(await exec("--help")).toContain(VERSION);
});

it("can be invoked for version", async () => {
expect(await exec("-V")).toEqual(`${VERSION}\n`);
expect(await exec("--version")).toEqual(`${VERSION}\n`);
});

describe("run", () => {
it("evaluates a module and prints it", async () => {
expect(await exec("run test/modules/1.yaml")).toEqual("1\n");
});
});
});

async function exec(str: string) {
let p = Deno.run({
stdout: "piped",
stderr: "piped",
cmd: ["deno", "run", "-A", "cli/pls.ts", ...str.split(/\s+/)],
});
try {
let status = await p.status();
let output = new TextDecoder().decode(await p.output());
if (!status.success) {
throw new Error(`${status}: ${output}`);
} else {
return output;
}
} finally {
p.close();
p.stderr.close();
}
}
2 changes: 1 addition & 1 deletion www/components/main-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function createMainPage<TProps extends MainPageProps = MainPageProps>(
</Head>
<div class="h-screen flex flex-col">
<Header className="flex justify-between" active={props.data.active} />
<main style={{flexGrow: 1}}>
<main style={{ flexGrow: 1 }}>
<Component {...props} />
</main>
<Footer className="border(t-2 gray-200) bg-gray-100 h-32 flex flex-col gap-4 justify-center" />
Expand Down
Loading