# Lint phase. The linters are invoked directly rather than through `make
# lint` or `script/lint`, which are themselves a docker build and would
# recurse into a daemon that does not exist in a build step.
#
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS lint

WORKDIR /app

COPY script/ script/
COPY package.json yarn.lock ./
RUN script/bootstrap

COPY . .

RUN yarn run eslint .
RUN yarn run prettier --check .

# Test phase, same shape and for the same reason. The suite runs without
# verbose output first and is rerun verbosely only if it fails; the timeout
# catches a hung test.
#
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS test

WORKDIR /app

COPY script/ script/
COPY package.json yarn.lock ./
RUN script/bootstrap

COPY . .

# Unlike the template, the suite runs as the image's non-root `node` user:
# root ignores directory permissions, so the tests of a destination that is
# not writable would otherwise be skipped. vitest writes into /app.
RUN chown -R node:node /app
USER node

RUN timeout 90 yarn run vitest run --reporter=dot || \
    { echo "--- Rerunning with verbose for details ---"; \
      timeout 90 yarn run vitest run --reporter=verbose; exit 1; }

# Build stage, and the last stage: a plain `docker build .` names no target
# and so builds this one. Nothing is wanted from the two phases above; the
# copies are what make BuildKit build them first, so this image cannot be
# produced unless lint and test passed. A stage appended after this one
# would drop all three out of a plain build.
#
# node 22.22.0 on Alpine 3.23.3 (node:22-alpine), 2026-08-09
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34

WORKDIR /app

COPY --from=lint /app/package.json /dev/null
COPY --from=test /app/package.json /dev/null

COPY script/ script/
COPY package.json yarn.lock ./
RUN script/bootstrap

COPY . .

# The version is computed on the host and passed in, because
# .dockerignore excludes .git.
ARG VERSION=dev
LABEL org.opencontainers.image.version="${VERSION}"

RUN make build
