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
16 changes: 16 additions & 0 deletions sandbox/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Loads `@deno/sandbox` lazily, at command run time.
*
* The `deno deploy` subcommand executes this package with the user's
* workspace config applied, so an eager import would make Deno consider a
* workspace member named `@deno/sandbox` for the CLI's own module graph: a
* member whose version satisfies the constraint gets linked in place of the
* published package, and a non-matching one emits a "Workspace member ...
* was not used" warning — for every command, even ones that never touch
* sandboxes. Keeping the import dynamic confines both effects to the
* `sandbox` subcommands. Type-only imports of `@deno/sandbox` are erased at
* runtime and remain safe anywhere.
*/
export function sandboxApi(): Promise<typeof import("@deno/sandbox")> {
return import("@deno/sandbox");
}
10 changes: 4 additions & 6 deletions sandbox/mod.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Command, ValidationError } from "@cliffy/command";
import {
type Region,
Sandbox,
type VolumeId,
type VolumeSlug,
} from "@deno/sandbox";
import type { Region, Sandbox, VolumeId, VolumeSlug } from "@deno/sandbox";
import { sandboxApi } from "./api.ts";
import { green, magenta, red, setColorEnabled, yellow } from "@std/fmt/colors";
import { pooledMap } from "@std/async";
import { expandGlob } from "@std/fs";
Expand Down Expand Up @@ -119,6 +115,7 @@ export const sandboxCreateCommand = new Command<SandboxContext>()
memory = Math.floor(parseSize(options, options.memory));
}

const { Sandbox } = await sandboxApi();
const sandbox = await Sandbox.create({
debug: options.debug,
token,
Expand Down Expand Up @@ -613,6 +610,7 @@ async function connectToSandbox(
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Sandbox } = await sandboxApi();
return await Sandbox.connect({
id: sandboxId,
apiEndpoint: options.endpoint,
Expand Down
8 changes: 7 additions & 1 deletion sandbox/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "@cliffy/command";
import type { SandboxContext } from "./mod.ts";
import { getAuth } from "../auth.ts";
import { Client } from "@deno/sandbox";
import { sandboxApi } from "./api.ts";
import { formatSize, tablePrinter, writeJsonResult } from "../util.ts";
import { green } from "@std/fmt/colors";
import { actionHandler, getOrg } from "../config.ts";
Expand All @@ -15,6 +15,8 @@ export const snapshotsCreateCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand All @@ -40,6 +42,8 @@ export const snapshotsListCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand Down Expand Up @@ -92,6 +96,8 @@ export const snapshotsDeleteCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand Down
10 changes: 9 additions & 1 deletion sandbox/volumes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from "@cliffy/command";
import type { SandboxContext } from "./mod.ts";
import { getAuth } from "../auth.ts";
import { Client } from "@deno/sandbox";
import { sandboxApi } from "./api.ts";
import {
formatSize,
parseSize,
Expand All @@ -28,6 +28,8 @@ export const volumesCreateCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand Down Expand Up @@ -57,6 +59,8 @@ export const volumesListCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand Down Expand Up @@ -108,6 +112,8 @@ export const volumesDeleteCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand All @@ -132,6 +138,8 @@ export const volumesSnapshotCommand = new Command<SandboxContext>()
const org = await getOrg(options, config, options.org);
const token = await getAuth(options, true);

const { Client } = await sandboxApi();

const client = new Client({
apiEndpoint: options.endpoint,
token,
Expand Down
54 changes: 54 additions & 0 deletions tests/lazy_sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { assert } from "@std/assert";
import { fromFileUrl } from "@std/path";

// `deno deploy` runs this package with the user's workspace config applied,
// so an eager (static, non-type) import of @deno/sandbox would let a
// workspace member named @deno/sandbox be linked into the CLI's module graph
// (or emit a "workspace member was not used" warning) for every command.
// Assert that first-party code only reaches @deno/sandbox through dynamic
// imports; type-only imports are erased at runtime and are fine.

const MAIN_TS = fromFileUrl(new URL("../main.ts", import.meta.url));

interface InfoDependency {
specifier: string;
isDynamic?: boolean;
code?: { specifier?: string };
}

interface InfoModule {
specifier: string;
dependencies?: InfoDependency[];
}

Deno.test("@deno/sandbox is not in the CLI's eager module graph", async () => {
const { code, stdout, stderr } = await new Deno.Command(Deno.execPath(), {
args: ["info", "--json", MAIN_TS],
stdout: "piped",
stderr: "piped",
}).output();
assert(code === 0, new TextDecoder().decode(stderr));

const graph = JSON.parse(new TextDecoder().decode(stdout)) as {
modules: InfoModule[];
};

const offenders: string[] = [];
for (const mod of graph.modules) {
// Only first-party modules matter; edges inside the @deno/sandbox
// package itself are unreachable unless the dynamic import runs.
if (!mod.specifier.startsWith("file://")) continue;
for (const dep of mod.dependencies ?? []) {
const target = dep.code?.specifier ?? "";
if (target.includes("@deno/sandbox") && !dep.isDynamic) {
offenders.push(`${mod.specifier} -> ${target}`);
}
}
}

assert(
offenders.length === 0,
`static @deno/sandbox import(s) found (use sandbox/api.ts's lazy ` +
`sandboxApi() or an \`import type\` instead):\n${offenders.join("\n")}`,
);
});
Loading