@vercel/container
Advanced tools
+108
-25
@@ -90,2 +90,13 @@ "use strict"; | ||
| } | ||
| var DOCKERFILE_CANDIDATES = [ | ||
| "Dockerfile.vercel", | ||
| "Containerfile.vercel" | ||
| ]; | ||
| function findDockerfile(workPath) { | ||
| return DOCKERFILE_CANDIDATES.find((name) => (0, import_node_fs.existsSync)((0, import_node_path.join)(workPath, name))); | ||
| } | ||
| function devImageTag(serviceName) { | ||
| const safe = serviceName.toLowerCase().replace(/[^a-z0-9-_.]/g, "-"); | ||
| return `vercel-dev/${safe || "service"}:dev`; | ||
| } | ||
| function run(cmd, args, opts = {}) { | ||
@@ -1151,6 +1162,2 @@ return new Promise((resolve, reject) => { | ||
| } | ||
| function devImageTag(serviceName) { | ||
| const safe = serviceName.toLowerCase().replace(/[^a-z0-9-_.]/g, "-"); | ||
| return `vercel-dev/${safe || "service"}:dev`; | ||
| } | ||
| function normalizeCommand(command) { | ||
@@ -1168,3 +1175,3 @@ if (typeof command === "string") { | ||
| const entrypointRef = readString(entrypoint); | ||
| const dockerfileConfigured = entrypointRef && isDockerfileRef(entrypointRef) ? entrypointRef : void 0; | ||
| const dockerfileConfigured = entrypointRef && isDockerfileRef(entrypointRef) ? entrypointRef : findDockerfile(workPath); | ||
| const dockerfileRel = dockerfileConfigured ?? "Dockerfile"; | ||
@@ -1248,3 +1255,63 @@ const dockerfilePath = import_node_path4.default.join(workPath, dockerfileRel); | ||
| } | ||
| async function assertDockerAvailable(out) { | ||
| try { | ||
| await runForwarded( | ||
| "docker", | ||
| ["info", "--format", "{{.ServerVersion}}"], | ||
| out, | ||
| { | ||
| quiet: true | ||
| } | ||
| ); | ||
| } catch (err) { | ||
| const message = err.message ?? ""; | ||
| if (/command not found/i.test(message)) { | ||
| throw new Error( | ||
| "Docker is required for `vercel dev` with containers, but the `docker` command was not found. Install Docker and ensure it is on your PATH." | ||
| ); | ||
| } | ||
| throw new Error( | ||
| "Could not connect to the Docker daemon. Start Docker (e.g. open Docker Desktop) and run `vercel dev` again." | ||
| ); | ||
| } | ||
| } | ||
| function containerExitMessage(exitCode, stderr) { | ||
| const detail = stderr.trim().split("\n").slice(-5).join("\n"); | ||
| const looksLikeDaemonDown = exitCode === 125 || /cannot connect to the docker daemon/i.test(stderr); | ||
| if (looksLikeDaemonDown) { | ||
| 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 ? ` | ||
| Docker reported: | ||
| ${detail}` : ""); | ||
| } | ||
| return `The container exited (code ${exitCode}) before becoming ready.` + (detail ? ` | ||
| Docker reported: | ||
| ${detail}` : ""); | ||
| } | ||
| var runningContainers = /* @__PURE__ */ new Map(); | ||
| var pendingContainers = /* @__PURE__ */ new Map(); | ||
| function containerReuseKey(options) { | ||
| return options.service?.name ?? `root:${options.workPath}`; | ||
| } | ||
| async function startDevServer(options) { | ||
| const reuseKey = containerReuseKey(options); | ||
| const existing = runningContainers.get(reuseKey); | ||
| if (existing && existing.isRunning()) { | ||
| return existing.result; | ||
| } | ||
| if (existing) { | ||
| runningContainers.delete(reuseKey); | ||
| } | ||
| const inFlight = pendingContainers.get(reuseKey); | ||
| if (inFlight) { | ||
| return inFlight; | ||
| } | ||
| const startPromise = startContainer(options, reuseKey).finally(() => { | ||
| pendingContainers.delete(reuseKey); | ||
| }); | ||
| pendingContainers.set(reuseKey, startPromise); | ||
| return startPromise; | ||
| } | ||
| async function startContainer(options, reuseKey) { | ||
| return withSpan( | ||
@@ -1257,2 +1324,3 @@ options.span, | ||
| const out = { onStdout, onStderr }; | ||
| await assertDockerAvailable(out); | ||
| const image = await withSpan( | ||
@@ -1305,4 +1373,13 @@ span, | ||
| }); | ||
| let runStderr = ""; | ||
| child.stdout?.on("data", (data) => onStdout?.(data)); | ||
| child.stderr?.on("data", (data) => onStderr?.(data)); | ||
| child.stderr?.on("data", (data) => { | ||
| runStderr += data.toString(); | ||
| onStderr?.(data); | ||
| }); | ||
| child.on("error", (err) => { | ||
| if (err.code === "ENOENT") { | ||
| runStderr += "Command not found: `docker`. Ensure Docker is installed and on your PATH, and that the Docker daemon is running."; | ||
| } | ||
| }); | ||
| const cleanupEnvFile = () => { | ||
@@ -1312,2 +1389,3 @@ (0, import_node_fs4.rmSync)(import_node_path4.default.dirname(envFilePath), { recursive: true, force: true }); | ||
| const shutdown = async () => { | ||
| runningContainers.delete(reuseKey); | ||
| try { | ||
@@ -1331,5 +1409,3 @@ await runForwarded("docker", ["stop", containerName], out, { | ||
| if (child.exitCode !== null) { | ||
| throw new Error( | ||
| `Container "${options.service?.name}" exited (code ${child.exitCode}) before becoming ready.` | ||
| ); | ||
| throw new Error(containerExitMessage(child.exitCode, runStderr)); | ||
| } | ||
@@ -1350,3 +1426,3 @@ try { | ||
| throw new Error( | ||
| `Timed out waiting for container "${options.service?.name}" to publish port ${containerPort}.` + (lastErr ? ` Last error: ${lastErr.message}` : "") | ||
| `Timed out waiting for container "${containerName}" to publish port ${containerPort}.` + (lastErr ? ` Last error: ${lastErr.message}` : "") | ||
| ); | ||
@@ -1364,7 +1440,22 @@ } | ||
| emit(out, `\u25B2 container container ready on localhost:${hostPort}`); | ||
| return { | ||
| const result = { | ||
| port: hostPort, | ||
| pid: child.pid ?? 0, | ||
| shutdown | ||
| shutdown, | ||
| // The container is a long-running server; the dev server should keep it | ||
| // alive across requests instead of tearing it down after each response. | ||
| persistent: true | ||
| }; | ||
| const running = { | ||
| result, | ||
| containerName, | ||
| isRunning: () => child.exitCode === null | ||
| }; | ||
| child.on("close", () => { | ||
| if (runningContainers.get(reuseKey) === running) { | ||
| runningContainers.delete(reuseKey); | ||
| } | ||
| }); | ||
| runningContainers.set(reuseKey, running); | ||
| return result; | ||
| } | ||
@@ -1413,8 +1504,2 @@ ); | ||
| } | ||
| var DOCKERFILE_CANDIDATES = ["Dockerfile.vercel", "Containerfile.vercel"]; | ||
| function findDockerfile(workPath) { | ||
| return DOCKERFILE_CANDIDATES.find( | ||
| (name) => (0, import_node_fs6.existsSync)(import_node_path6.default.join(workPath, name)) | ||
| ); | ||
| } | ||
| function sanitizeRepository(name) { | ||
@@ -1603,10 +1688,8 @@ const sanitized = name.toLowerCase().replace(/[^a-z0-9-_./]/g, "-").replace(/-+/g, "-").replace(/(^[-/.]+)|([-/.]+$)/g, ""); | ||
| if (meta?.isDev) { | ||
| if (prebuiltImage) { | ||
| span?.setAttributes({ "container.mode": "prebuilt_dev" }); | ||
| info(`vercel dev: using prebuilt image ${prebuiltImage}`); | ||
| return prebuiltImage; | ||
| } | ||
| throw new Error( | ||
| '`vercel dev` cannot build container images from a Dockerfile. Specify a prebuilt "image" for local development.' | ||
| const serviceName2 = options.service?.name; | ||
| const tag2 = devImageTag( | ||
| serviceName2 ?? import_node_path6.default.basename(dockerfileRel).split(".")[0] | ||
| ); | ||
| span?.setAttributes({ "container.mode": "dev", "image.tag": tag2 }); | ||
| return tag2; | ||
| } | ||
@@ -1613,0 +1696,0 @@ if (!(0, import_node_fs6.existsSync)(dockerfilePath)) { |
+2
-2
| { | ||
| "name": "@vercel/container", | ||
| "version": "0.0.3", | ||
| "version": "0.0.4", | ||
| "license": "Apache-2.0", | ||
@@ -18,3 +18,3 @@ "main": "./dist/index.js", | ||
| "vitest": "2.0.3", | ||
| "@vercel/build-utils": "13.32.1" | ||
| "@vercel/build-utils": "13.32.2" | ||
| }, | ||
@@ -21,0 +21,0 @@ "publishConfig": { |
70070
4.16%1742
4.88%