Skip to content

Commit 9fec60c

Browse files
authored
fix(sandbox): load @deno/sandbox lazily (#130)
deno deploy executes this package with the user's workspace config applied, so the CLI's static import of @deno/sandbox let a workspace member named @deno/sandbox be linked into the CLI's own module graph (crashing the CLI) or emit a "workspace member was not used" warning on every command. * import @deno/sandbox only via a dynamic import in sandbox/api.ts, executed when a sandbox command actually runs * remaining references are import type, erased at runtime * adds a regression test asserting no first-party module has a static code edge to @deno/sandbox Root cause tracked upstream in denoland/deno#35730.
1 parent b396737 commit 9fec60c

5 files changed

Lines changed: 90 additions & 8 deletions

File tree

sandbox/api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Loads `@deno/sandbox` lazily, at command run time.
3+
*
4+
* The `deno deploy` subcommand executes this package with the user's
5+
* workspace config applied, so an eager import would make Deno consider a
6+
* workspace member named `@deno/sandbox` for the CLI's own module graph: a
7+
* member whose version satisfies the constraint gets linked in place of the
8+
* published package, and a non-matching one emits a "Workspace member ...
9+
* was not used" warning — for every command, even ones that never touch
10+
* sandboxes. Keeping the import dynamic confines both effects to the
11+
* `sandbox` subcommands. Type-only imports of `@deno/sandbox` are erased at
12+
* runtime and remain safe anywhere.
13+
*/
14+
export function sandboxApi(): Promise<typeof import("@deno/sandbox")> {
15+
return import("@deno/sandbox");
16+
}

sandbox/mod.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { Command, ValidationError } from "@cliffy/command";
2-
import {
3-
type Region,
4-
Sandbox,
5-
type VolumeId,
6-
type VolumeSlug,
7-
} from "@deno/sandbox";
2+
import type { Region, Sandbox, VolumeId, VolumeSlug } from "@deno/sandbox";
3+
import { sandboxApi } from "./api.ts";
84
import { green, magenta, red, setColorEnabled, yellow } from "@std/fmt/colors";
95
import { pooledMap } from "@std/async";
106
import { expandGlob } from "@std/fs";
@@ -119,6 +115,7 @@ export const sandboxCreateCommand = new Command<SandboxContext>()
119115
memory = Math.floor(parseSize(options, options.memory));
120116
}
121117

118+
const { Sandbox } = await sandboxApi();
122119
const sandbox = await Sandbox.create({
123120
debug: options.debug,
124121
token,
@@ -613,6 +610,7 @@ async function connectToSandbox(
613610
const org = await getOrg(options, config, options.org);
614611
const token = await getAuth(options, true);
615612

613+
const { Sandbox } = await sandboxApi();
616614
return await Sandbox.connect({
617615
id: sandboxId,
618616
apiEndpoint: options.endpoint,

sandbox/snapshot.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "@cliffy/command";
22
import type { SandboxContext } from "./mod.ts";
33
import { getAuth } from "../auth.ts";
4-
import { Client } from "@deno/sandbox";
4+
import { sandboxApi } from "./api.ts";
55
import { formatSize, tablePrinter, writeJsonResult } from "../util.ts";
66
import { green } from "@std/fmt/colors";
77
import { actionHandler, getOrg } from "../config.ts";
@@ -15,6 +15,8 @@ export const snapshotsCreateCommand = new Command<SandboxContext>()
1515
const org = await getOrg(options, config, options.org);
1616
const token = await getAuth(options, true);
1717

18+
const { Client } = await sandboxApi();
19+
1820
const client = new Client({
1921
apiEndpoint: options.endpoint,
2022
token,
@@ -40,6 +42,8 @@ export const snapshotsListCommand = new Command<SandboxContext>()
4042
const org = await getOrg(options, config, options.org);
4143
const token = await getAuth(options, true);
4244

45+
const { Client } = await sandboxApi();
46+
4347
const client = new Client({
4448
apiEndpoint: options.endpoint,
4549
token,
@@ -92,6 +96,8 @@ export const snapshotsDeleteCommand = new Command<SandboxContext>()
9296
const org = await getOrg(options, config, options.org);
9397
const token = await getAuth(options, true);
9498

99+
const { Client } = await sandboxApi();
100+
95101
const client = new Client({
96102
apiEndpoint: options.endpoint,
97103
token,

sandbox/volumes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Command } from "@cliffy/command";
22
import type { SandboxContext } from "./mod.ts";
33
import { getAuth } from "../auth.ts";
4-
import { Client } from "@deno/sandbox";
4+
import { sandboxApi } from "./api.ts";
55
import {
66
formatSize,
77
parseSize,
@@ -28,6 +28,8 @@ export const volumesCreateCommand = new Command<SandboxContext>()
2828
const org = await getOrg(options, config, options.org);
2929
const token = await getAuth(options, true);
3030

31+
const { Client } = await sandboxApi();
32+
3133
const client = new Client({
3234
apiEndpoint: options.endpoint,
3335
token,
@@ -57,6 +59,8 @@ export const volumesListCommand = new Command<SandboxContext>()
5759
const org = await getOrg(options, config, options.org);
5860
const token = await getAuth(options, true);
5961

62+
const { Client } = await sandboxApi();
63+
6064
const client = new Client({
6165
apiEndpoint: options.endpoint,
6266
token,
@@ -108,6 +112,8 @@ export const volumesDeleteCommand = new Command<SandboxContext>()
108112
const org = await getOrg(options, config, options.org);
109113
const token = await getAuth(options, true);
110114

115+
const { Client } = await sandboxApi();
116+
111117
const client = new Client({
112118
apiEndpoint: options.endpoint,
113119
token,
@@ -132,6 +138,8 @@ export const volumesSnapshotCommand = new Command<SandboxContext>()
132138
const org = await getOrg(options, config, options.org);
133139
const token = await getAuth(options, true);
134140

141+
const { Client } = await sandboxApi();
142+
135143
const client = new Client({
136144
apiEndpoint: options.endpoint,
137145
token,

tests/lazy_sandbox.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { assert } from "@std/assert";
2+
import { fromFileUrl } from "@std/path";
3+
4+
// `deno deploy` runs this package with the user's workspace config applied,
5+
// so an eager (static, non-type) import of @deno/sandbox would let a
6+
// workspace member named @deno/sandbox be linked into the CLI's module graph
7+
// (or emit a "workspace member was not used" warning) for every command.
8+
// Assert that first-party code only reaches @deno/sandbox through dynamic
9+
// imports; type-only imports are erased at runtime and are fine.
10+
11+
const MAIN_TS = fromFileUrl(new URL("../main.ts", import.meta.url));
12+
13+
interface InfoDependency {
14+
specifier: string;
15+
isDynamic?: boolean;
16+
code?: { specifier?: string };
17+
}
18+
19+
interface InfoModule {
20+
specifier: string;
21+
dependencies?: InfoDependency[];
22+
}
23+
24+
Deno.test("@deno/sandbox is not in the CLI's eager module graph", async () => {
25+
const { code, stdout, stderr } = await new Deno.Command(Deno.execPath(), {
26+
args: ["info", "--json", MAIN_TS],
27+
stdout: "piped",
28+
stderr: "piped",
29+
}).output();
30+
assert(code === 0, new TextDecoder().decode(stderr));
31+
32+
const graph = JSON.parse(new TextDecoder().decode(stdout)) as {
33+
modules: InfoModule[];
34+
};
35+
36+
const offenders: string[] = [];
37+
for (const mod of graph.modules) {
38+
// Only first-party modules matter; edges inside the @deno/sandbox
39+
// package itself are unreachable unless the dynamic import runs.
40+
if (!mod.specifier.startsWith("file://")) continue;
41+
for (const dep of mod.dependencies ?? []) {
42+
const target = dep.code?.specifier ?? "";
43+
if (target.includes("@deno/sandbox") && !dep.isDynamic) {
44+
offenders.push(`${mod.specifier} -> ${target}`);
45+
}
46+
}
47+
}
48+
49+
assert(
50+
offenders.length === 0,
51+
`static @deno/sandbox import(s) found (use sandbox/api.ts's lazy ` +
52+
`sandboxApi() or an \`import type\` instead):\n${offenders.join("\n")}`,
53+
);
54+
});

0 commit comments

Comments
 (0)