Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 31044c3

Browse files
committed
Add interactive REPL, ref cloudflare/workers-sdk#1263
Adds a `--repl` flag to start an interactive REPL. Allows any other option to be passed alongside, and automatically loads bindings from `wrangler.toml` files too. Specifying a script is optional when `--repl` is enabled.
1 parent 20b7159 commit 31044c3

File tree

10 files changed

+74
-20
lines changed

10 files changed

+74
-20
lines changed

packages/cli-parser/test/help.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Core Options:
4848
-V, --verbose Enable verbose logging [boolean]
4949
--(no-)update-check Enable update checker (enabled by [boolean]
5050
default)
51+
--repl Enable interactive REPL [boolean]
5152
--root Path to resolve files relative to [string]
5253
--mount Mount additional named [array:NAME=PATH[@ENV]]
5354
workers

packages/core/src/index.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -828,19 +828,21 @@ export class MiniflareCore<
828828

829829
// Log bundle size and warning if too big
830830
// noinspection JSObjectNullOrUndefined
831-
this.#ctx.log.info(
832-
`Worker reloaded!${
833-
res?.bundleSize !== undefined ? ` (${formatSize(res.bundleSize)})` : ""
834-
}`
835-
);
836-
// TODO (someday): compress asynchronously
837-
// noinspection JSObjectNullOrUndefined
838-
if (res?.bundleSize !== undefined && res.bundleSize > 1_048_576) {
839-
this.#ctx.log.warn(
840-
"Worker's uncompressed size exceeds the 1MiB limit! " +
841-
"Note that your worker will be compressed during upload " +
842-
"so you may still be able to deploy it."
831+
if (res) {
832+
this.#ctx.log.info(
833+
`Worker reloaded!${
834+
res.bundleSize !== undefined ? ` (${formatSize(res.bundleSize)})` : ""
835+
}`
843836
);
837+
// TODO (someday): compress asynchronously
838+
// noinspection JSObjectNullOrUndefined
839+
if (res.bundleSize !== undefined && res.bundleSize > 1_048_576) {
840+
this.#ctx.log.warn(
841+
"Worker's uncompressed size exceeds the 1MiB limit! " +
842+
"Note that your worker will be compressed during upload " +
843+
"so you may still be able to deploy it."
844+
);
845+
}
844846
}
845847

846848
// Update watched paths if watching

packages/core/src/plugins/core.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface CoreOptions {
104104
debug?: boolean;
105105
verbose?: boolean;
106106
updateCheck?: boolean;
107+
repl?: boolean;
107108
// Replaced in MiniflareCoreOptions with something plugins-specific
108109
mounts?: Record<string, string | CoreOptions | BindingsOptions>;
109110
name?: string;
@@ -286,6 +287,12 @@ export class CorePlugin extends Plugin<CoreOptions> implements CoreOptions {
286287
})
287288
updateCheck?: boolean;
288289

290+
@Option({
291+
type: OptionType.BOOLEAN,
292+
description: "Enable interactive REPL",
293+
})
294+
repl?: boolean;
295+
289296
@Option({
290297
type: OptionType.STRING,
291298
name: "root",

packages/core/test/index.mounts.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@ test("MiniflareCore: dispose: disposes of mounts too", async (t) => {
418418
[LogLevel.INFO, "Worker reloaded! (2B)"],
419419
[LogLevel.DEBUG, "Mount Routes: <none>"],
420420
[LogLevel.DEBUG, "Reloading worker..."],
421-
[LogLevel.INFO, "Worker reloaded!"],
422421
]);
423422

424423
log.logs = [];

packages/core/test/index.spec.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,6 @@ test("MiniflareCore: #watcherCallback: re-runs setup for plugins' changed paths"
808808
[LogLevel.DEBUG, "Reloading worker..."],
809809
[LogLevel.INFO, "beforeReload"],
810810
[LogLevel.INFO, "reload"],
811-
[LogLevel.INFO, "Worker reloaded!"],
812811
]);
813812

814813
// Update test2 contents, expecting TestPlugin setup to run
@@ -822,7 +821,6 @@ test("MiniflareCore: #watcherCallback: re-runs setup for plugins' changed paths"
822821
[LogLevel.DEBUG, "Reloading worker..."],
823822
[LogLevel.INFO, "beforeReload"],
824823
[LogLevel.INFO, "reload"],
825-
[LogLevel.INFO, "Worker reloaded!"],
826824
]);
827825
});
828826
test("MiniflareCore: #watcherCallback: re-runs setup for script-providing plugins if any beforeSetup ran", async (t) => {
@@ -959,7 +957,6 @@ test("MiniflareCore: setOptions: updates options and reloads worker", async (t)
959957
[LogLevel.INFO, "beforeReload"],
960958
[LogLevel.VERBOSE, "- reload(TestPlugin)"],
961959
[LogLevel.INFO, "reload"],
962-
[LogLevel.INFO, "Worker reloaded!"],
963960
];
964961
t.deepEqual(log.logs, expectedLogs);
965962
});

packages/core/test/plugins/core.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ test("CorePlugin: parses options from argv", (t) => {
5454
"--watch",
5555
"--debug",
5656
"--verbose",
57+
"--no-update-check",
58+
"--repl",
5759
"--root",
5860
"root",
5961
"--mount",
@@ -91,6 +93,8 @@ test("CorePlugin: parses options from argv", (t) => {
9193
watch: true,
9294
debug: true,
9395
verbose: true,
96+
updateCheck: false,
97+
repl: true,
9498
rootPath: "root",
9599
mounts: {
96100
api: {
@@ -194,6 +198,7 @@ test("CorePlugin: parses options from wrangler config", async (t) => {
194198
debug: undefined,
195199
verbose: undefined,
196200
updateCheck: false,
201+
repl: undefined,
197202
rootPath: undefined,
198203
mounts: {
199204
api: {

packages/miniflare/src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type MiniflareOptions = Omit<
6464
> & {
6565
log?: Log;
6666
sourceMap?: boolean;
67+
scriptRequired?: boolean;
6768
};
6869

6970
export class Miniflare extends MiniflareCore<Plugins> {
@@ -84,7 +85,7 @@ export class Miniflare extends MiniflareCore<Plugins> {
8485
log: options?.log ?? new NoOpLog(),
8586
storageFactory,
8687
scriptRunner: new VMScriptRunner(),
87-
scriptRequired: true,
88+
scriptRequired: options?.scriptRequired ?? true,
8889
},
8990
options
9091
);

packages/miniflare/src/cli.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Options } from "@miniflare/shared";
55
import { red } from "kleur/colors";
66
import type { MiniflareOptions } from "miniflare";
77
import open from "open";
8+
import { startREPL } from "./repl";
89
import { updateCheck } from "./updater";
910

1011
function suppressWarnings() {
@@ -89,12 +90,27 @@ async function main() {
8990
mfOptions.sourceMap = true;
9091
// Catch and log unhandled rejections as opposed to crashing
9192
mfOptions.logUnhandledRejections = true;
93+
if (mfOptions.repl) {
94+
// Allow REPL to be started without a script
95+
mfOptions.scriptRequired = false;
96+
// Disable file watching in REPL
97+
mfOptions.watch = false;
98+
// Allow async I/O in REPL without request context
99+
mfOptions.globalAsyncIO = true;
100+
mfOptions.globalTimers = true;
101+
mfOptions.globalRandom = true;
102+
}
92103

93104
const mf = new Miniflare(mfOptions);
94105
try {
95-
// Start Miniflare development server
96-
await mf.startServer();
97-
await mf.startScheduler();
106+
if (mfOptions.repl) {
107+
// Start Miniflare REPL
108+
await startREPL(mf);
109+
} else {
110+
// Start Miniflare development server
111+
await mf.startServer();
112+
await mf.startScheduler();
113+
}
98114
} catch (e: any) {
99115
mf.log.error(e);
100116
process.exitCode = 1;

packages/miniflare/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./api";
22
export * from "./storage";
3+
export * from "./repl";
34
export * from "./updater";
45
export { Log, LogLevel } from "@miniflare/shared";
56
export { Request, Response } from "@miniflare/core";

packages/miniflare/src/repl.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import repl from "repl";
2+
import vm from "vm";
3+
import { CorePluginSignatures, MiniflareCore } from "@miniflare/core";
4+
5+
interface MutableContextREPLServer extends repl.REPLServer {
6+
context: vm.Context;
7+
}
8+
9+
export async function startREPL<Plugins extends CorePluginSignatures>(
10+
mf: MiniflareCore<Plugins>
11+
): Promise<void> {
12+
// Get global scope and bindings
13+
const globalScope = await mf.getGlobalScope();
14+
const bindings = await mf.getBindings();
15+
16+
// Create custom context
17+
const context = vm.createContext(globalScope, {
18+
codeGeneration: { strings: false, wasm: false },
19+
});
20+
// Assign `env` as a global variable so people can use module worker's syntax
21+
context.env = bindings;
22+
23+
// Start the REPL with the custom context
24+
(repl.start() as MutableContextREPLServer).context = context;
25+
}

0 commit comments

Comments
 (0)