@vercel/container 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +108 -25
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -87,6 +87,17 @@ function isDockerfileRef(ref) {
87
87
  const base = (0, import_node_path.basename)(ref).toLowerCase();
88
88
  return base === "dockerfile" || base === "containerfile" || base === "dockerfile.vercel" || base === "containerfile.vercel";
89
89
  }
90
+ var DOCKERFILE_CANDIDATES = [
91
+ "Dockerfile.vercel",
92
+ "Containerfile.vercel"
93
+ ];
94
+ function findDockerfile(workPath) {
95
+ return DOCKERFILE_CANDIDATES.find((name) => (0, import_node_fs.existsSync)((0, import_node_path.join)(workPath, name)));
96
+ }
97
+ function devImageTag(serviceName) {
98
+ const safe = serviceName.toLowerCase().replace(/[^a-z0-9-_.]/g, "-");
99
+ return `vercel-dev/${safe || "service"}:dev`;
100
+ }
90
101
  function run(cmd, args, opts = {}) {
91
102
  return new Promise((resolve, reject) => {
92
103
  const child = (0, import_node_child_process.spawn)(cmd, args, {
@@ -1147,10 +1158,6 @@ ${detail}` : "")
1147
1158
  });
1148
1159
  });
1149
1160
  }
1150
- function devImageTag(serviceName) {
1151
- const safe = serviceName.toLowerCase().replace(/[^a-z0-9-_.]/g, "-");
1152
- return `vercel-dev/${safe || "service"}:dev`;
1153
- }
1154
1161
  function normalizeCommand(command) {
1155
1162
  if (typeof command === "string") {
1156
1163
  return [command];
@@ -1163,7 +1170,7 @@ function normalizeCommand(command) {
1163
1170
  async function resolveDevImage(options, out, span) {
1164
1171
  const { config, workPath, entrypoint } = options;
1165
1172
  const entrypointRef = readString(entrypoint);
1166
- const dockerfileConfigured = entrypointRef && isDockerfileRef(entrypointRef) ? entrypointRef : void 0;
1173
+ const dockerfileConfigured = entrypointRef && isDockerfileRef(entrypointRef) ? entrypointRef : findDockerfile(workPath);
1167
1174
  const dockerfileRel = dockerfileConfigured ?? "Dockerfile";
1168
1175
  const dockerfilePath = import_node_path4.default.join(workPath, dockerfileRel);
1169
1176
  const hasDockerfile = dockerfileConfigured !== void 0 || (0, import_node_fs4.existsSync)(dockerfilePath);
@@ -1242,7 +1249,67 @@ function uniqueContainerName(serviceName) {
1242
1249
  const safe = serviceName.toLowerCase().replace(/[^a-z0-9-_.]/g, "-");
1243
1250
  return `vercel-dev-${safe || "service"}-${process.pid}-${Date.now().toString(36)}`;
1244
1251
  }
1252
+ async function assertDockerAvailable(out) {
1253
+ try {
1254
+ await runForwarded(
1255
+ "docker",
1256
+ ["info", "--format", "{{.ServerVersion}}"],
1257
+ out,
1258
+ {
1259
+ quiet: true
1260
+ }
1261
+ );
1262
+ } catch (err) {
1263
+ const message = err.message ?? "";
1264
+ if (/command not found/i.test(message)) {
1265
+ throw new Error(
1266
+ "Docker is required for `vercel dev` with containers, but the `docker` command was not found. Install Docker and ensure it is on your PATH."
1267
+ );
1268
+ }
1269
+ throw new Error(
1270
+ "Could not connect to the Docker daemon. Start Docker (e.g. open Docker Desktop) and run `vercel dev` again."
1271
+ );
1272
+ }
1273
+ }
1274
+ function containerExitMessage(exitCode, stderr) {
1275
+ const detail = stderr.trim().split("\n").slice(-5).join("\n");
1276
+ const looksLikeDaemonDown = exitCode === 125 || /cannot connect to the docker daemon/i.test(stderr);
1277
+ if (looksLikeDaemonDown) {
1278
+ return "Could not start the container: the Docker daemon is not running or is unreachable. Start Docker (e.g. open Docker Desktop) and try `vercel dev` again." + (detail ? `
1279
+
1280
+ Docker reported:
1281
+ ${detail}` : "");
1282
+ }
1283
+ return `The container exited (code ${exitCode}) before becoming ready.` + (detail ? `
1284
+
1285
+ Docker reported:
1286
+ ${detail}` : "");
1287
+ }
1288
+ var runningContainers = /* @__PURE__ */ new Map();
1289
+ var pendingContainers = /* @__PURE__ */ new Map();
1290
+ function containerReuseKey(options) {
1291
+ return options.service?.name ?? `root:${options.workPath}`;
1292
+ }
1245
1293
  async function startDevServer(options) {
1294
+ const reuseKey = containerReuseKey(options);
1295
+ const existing = runningContainers.get(reuseKey);
1296
+ if (existing && existing.isRunning()) {
1297
+ return existing.result;
1298
+ }
1299
+ if (existing) {
1300
+ runningContainers.delete(reuseKey);
1301
+ }
1302
+ const inFlight = pendingContainers.get(reuseKey);
1303
+ if (inFlight) {
1304
+ return inFlight;
1305
+ }
1306
+ const startPromise = startContainer(options, reuseKey).finally(() => {
1307
+ pendingContainers.delete(reuseKey);
1308
+ });
1309
+ pendingContainers.set(reuseKey, startPromise);
1310
+ return startPromise;
1311
+ }
1312
+ async function startContainer(options, reuseKey) {
1246
1313
  return withSpan(
1247
1314
  options.span,
1248
1315
  "container.dev.start",
@@ -1250,6 +1317,7 @@ async function startDevServer(options) {
1250
1317
  async (span) => {
1251
1318
  const { config, meta, onStdout, onStderr } = options;
1252
1319
  const out = { onStdout, onStderr };
1320
+ await assertDockerAvailable(out);
1253
1321
  const image = await withSpan(
1254
1322
  span,
1255
1323
  "container.dev.resolve_image",
@@ -1297,12 +1365,22 @@ async function startDevServer(options) {
1297
1365
  const child = (0, import_node_child_process3.spawn)("docker", args, {
1298
1366
  stdio: ["ignore", "pipe", "pipe"]
1299
1367
  });
1368
+ let runStderr = "";
1300
1369
  child.stdout?.on("data", (data) => onStdout?.(data));
1301
- child.stderr?.on("data", (data) => onStderr?.(data));
1370
+ child.stderr?.on("data", (data) => {
1371
+ runStderr += data.toString();
1372
+ onStderr?.(data);
1373
+ });
1374
+ child.on("error", (err) => {
1375
+ if (err.code === "ENOENT") {
1376
+ runStderr += "Command not found: `docker`. Ensure Docker is installed and on your PATH, and that the Docker daemon is running.";
1377
+ }
1378
+ });
1302
1379
  const cleanupEnvFile = () => {
1303
1380
  (0, import_node_fs4.rmSync)(import_node_path4.default.dirname(envFilePath), { recursive: true, force: true });
1304
1381
  };
1305
1382
  const shutdown = async () => {
1383
+ runningContainers.delete(reuseKey);
1306
1384
  try {
1307
1385
  await runForwarded("docker", ["stop", containerName], out, {
1308
1386
  quiet: true
@@ -1321,9 +1399,7 @@ async function startDevServer(options) {
1321
1399
  try {
1322
1400
  while (Date.now() < deadline) {
1323
1401
  if (child.exitCode !== null) {
1324
- throw new Error(
1325
- `Container "${options.service?.name}" exited (code ${child.exitCode}) before becoming ready.`
1326
- );
1402
+ throw new Error(containerExitMessage(child.exitCode, runStderr));
1327
1403
  }
1328
1404
  try {
1329
1405
  hostPort = await readMappedHostPort(
@@ -1339,7 +1415,7 @@ async function startDevServer(options) {
1339
1415
  }
1340
1416
  if (hostPort === void 0) {
1341
1417
  throw new Error(
1342
- `Timed out waiting for container "${options.service?.name}" to publish port ${containerPort}.` + (lastErr ? ` Last error: ${lastErr.message}` : "")
1418
+ `Timed out waiting for container "${containerName}" to publish port ${containerPort}.` + (lastErr ? ` Last error: ${lastErr.message}` : "")
1343
1419
  );
1344
1420
  }
1345
1421
  } catch (err) {
@@ -1352,11 +1428,26 @@ async function startDevServer(options) {
1352
1428
  "container.name": containerName
1353
1429
  });
1354
1430
  emit(out, `\u25B2 container container ready on localhost:${hostPort}`);
1355
- return {
1431
+ const result = {
1356
1432
  port: hostPort,
1357
1433
  pid: child.pid ?? 0,
1358
- shutdown
1434
+ shutdown,
1435
+ // The container is a long-running server; the dev server should keep it
1436
+ // alive across requests instead of tearing it down after each response.
1437
+ persistent: true
1359
1438
  };
1439
+ const running = {
1440
+ result,
1441
+ containerName,
1442
+ isRunning: () => child.exitCode === null
1443
+ };
1444
+ child.on("close", () => {
1445
+ if (runningContainers.get(reuseKey) === running) {
1446
+ runningContainers.delete(reuseKey);
1447
+ }
1448
+ });
1449
+ runningContainers.set(reuseKey, running);
1450
+ return result;
1360
1451
  }
1361
1452
  );
1362
1453
  }
@@ -1400,12 +1491,6 @@ function normalizeCommand2(command) {
1400
1491
  }
1401
1492
  return void 0;
1402
1493
  }
1403
- var DOCKERFILE_CANDIDATES = ["Dockerfile.vercel", "Containerfile.vercel"];
1404
- function findDockerfile(workPath) {
1405
- return DOCKERFILE_CANDIDATES.find(
1406
- (name) => (0, import_node_fs6.existsSync)(import_node_path6.default.join(workPath, name))
1407
- );
1408
- }
1409
1494
  function sanitizeRepository(name) {
1410
1495
  const sanitized = name.toLowerCase().replace(/[^a-z0-9-_./]/g, "-").replace(/-+/g, "-").replace(/(^[-/.]+)|([-/.]+$)/g, "");
1411
1496
  return sanitized || "service";
@@ -1589,14 +1674,12 @@ async function resolveImageHandler(options, span) {
1589
1674
  return prebuiltImage;
1590
1675
  }
1591
1676
  if (meta?.isDev) {
1592
- if (prebuiltImage) {
1593
- span?.setAttributes({ "container.mode": "prebuilt_dev" });
1594
- info(`vercel dev: using prebuilt image ${prebuiltImage}`);
1595
- return prebuiltImage;
1596
- }
1597
- throw new Error(
1598
- '`vercel dev` cannot build container images from a Dockerfile. Specify a prebuilt "image" for local development.'
1677
+ const serviceName2 = options.service?.name;
1678
+ const tag2 = devImageTag(
1679
+ serviceName2 ?? import_node_path6.default.basename(dockerfileRel).split(".")[0]
1599
1680
  );
1681
+ span?.setAttributes({ "container.mode": "dev", "image.tag": tag2 });
1682
+ return tag2;
1600
1683
  }
1601
1684
  if (!(0, import_node_fs6.existsSync)(dockerfilePath)) {
1602
1685
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/container",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "license": "Apache-2.0",
5
5
  "main": "./dist/index.js",
6
6
  "homepage": "https://vercel.com/docs",
@@ -15,7 +15,7 @@
15
15
  "devDependencies": {
16
16
  "@types/node": "20.11.0",
17
17
  "vitest": "2.0.3",
18
- "@vercel/build-utils": "13.32.1"
18
+ "@vercel/build-utils": "13.32.2"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public"