build(container): use turbo pruned web workspace #36

Merged
vince merged 2 commits from codex/turbo-pruned-container into main 2026-06-20 07:15:09 +02:00
6 changed files with 93 additions and 38 deletions

View file

@ -5,8 +5,18 @@ coverage
data data
dist dist
node_modules node_modules
out
playwright-report playwright-report
storybook-static storybook-static
test-results test-results
.env .env
.env.* .env.*
apps/*/.turbo
apps/*/build
apps/*/data
apps/*/dist
apps/*/playwright-report
apps/*/test-results
packages/*/.turbo
packages/*/dist
packages/*/storybook-static

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules/ node_modules/
out/
.svelte-kit/ .svelte-kit/
build/ build/
dist/ dist/

View file

@ -139,10 +139,11 @@ the checked-in `apps/web/drizzle/` directory so startup migrations can run.
### Internal Container ### Internal Container
The checked-in `apps/web/Containerfile` builds the React client and Bun server The checked-in `apps/web/Containerfile` runs
from the workspace root into a runtime image. For the Dimension Lab internal `turbo prune @dimensionlab/web --docker`, installs the pruned manifest set, and
host, run it behind Caddy on a loopback port and mount persistent state at builds the React client plus Bun server from the pruned workspace source. For the
`/data`: Dimension Lab internal host, run it behind Caddy on a loopback port and mount
persistent state at `/data`:
```sh ```sh
podman build -f apps/web/Containerfile -t localhost/dimensionlab-website:latest . podman build -f apps/web/Containerfile -t localhost/dimensionlab-website:latest .

View file

@ -1,19 +1,25 @@
FROM docker.io/oven/bun:1.3.14 AS deps FROM docker.io/oven/bun:1.3.14 AS base
WORKDIR /repo WORKDIR /repo
ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/dashboard-model/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH
COPY package.json bun.lock turbo.json tsconfig.base.json ./
COPY apps/web/package.json apps/web/package.json FROM base AS pruner
COPY packages/dashboard-model/package.json packages/dashboard-model/package.json
COPY packages/ui/package.json packages/ui/package.json COPY . .
RUN bunx turbo prune @dimensionlab/web --docker
FROM base AS deps
COPY --from=pruner /repo/out/json/ ./
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
FROM deps AS build FROM deps AS build
COPY . . COPY --from=pruner /repo/out/full/ ./
RUN bun install --frozen-lockfile && bun run build COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./
RUN bun run build
FROM deps AS runtime FROM base AS runtime
WORKDIR /repo/apps/web WORKDIR /repo/apps/web
ENV NODE_ENV=production ENV NODE_ENV=production

View file

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun src/server/dev.ts", "dev": "bun src/server/dev.ts",
"build": "vite build && bun build src/server/index.ts --target bun --outdir build", "build": "rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build",
"preview": "HOST=0.0.0.0 PORT=4173 bun build/index.js", "preview": "HOST=0.0.0.0 PORT=4173 bun build/index.js",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test": "vitest run", "test": "vitest run",

View file

@ -114,35 +114,72 @@ describe("workspace boundaries", () => {
); );
}); });
test("stages web workspace dependency manifests before container install", () => { test("builds the internal container from a turbo-pruned web workspace", () => {
const webPackage = JSON.parse(
readFileSync(join(root, "apps/web/package.json"), "utf8"),
) as { dependencies?: Record<string, string> };
const workspacePackages = [
"packages/ui/package.json",
"packages/dashboard-model/package.json",
].map((manifestPath) => {
const packageJson = JSON.parse(
readFileSync(join(root, manifestPath), "utf8"),
) as { name?: string };
return [packageJson.name, manifestPath] as const;
});
const manifestsByPackageName = new Map(workspacePackages);
const containerfile = readFileSync( const containerfile = readFileSync(
join(root, "apps/web/Containerfile"), join(root, "apps/web/Containerfile"),
"utf8", "utf8",
); );
const pruneCommand = "turbo prune @dimensionlab/web --docker";
const jsonCopy = "COPY --from=pruner /repo/out/json/ ./";
const sourceCopy = "COPY --from=pruner /repo/out/full/ ./";
const tsconfigCopy =
"COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./";
const installCommand = "RUN bun install --frozen-lockfile";
const workspaceDependencyManifests = Object.entries( expect(containerfile).toContain("AS pruner");
webPackage.dependencies ?? {}, expect(containerfile).toContain(pruneCommand);
) expect(containerfile).toContain(jsonCopy);
.filter(([, version]) => version.startsWith("workspace:")) expect(containerfile).toContain(sourceCopy);
.map(([packageName]) => manifestsByPackageName.get(packageName)); expect(containerfile).toContain(tsconfigCopy);
expect(containerfile).not.toContain(
"COPY packages/dashboard-model/package.json",
);
expect(containerfile).not.toContain("COPY packages/ui/package.json");
expect(workspaceDependencyManifests).not.toContain(undefined); const jsonCopyIndex = containerfile.indexOf(jsonCopy);
for (const manifestPath of workspaceDependencyManifests) { const installIndex = containerfile.indexOf(installCommand);
expect(containerfile).toContain(`COPY ${manifestPath} ${manifestPath}`); const sourceCopyIndex = containerfile.indexOf(sourceCopy);
} const tsconfigCopyIndex = containerfile.indexOf(tsconfigCopy);
expect(jsonCopyIndex).toBeGreaterThan(-1);
expect(installIndex).toBeGreaterThan(jsonCopyIndex);
expect(sourceCopyIndex).toBeGreaterThan(installIndex);
expect(tsconfigCopyIndex).toBeGreaterThan(sourceCopyIndex);
expect(containerfile.indexOf("RUN bun run build")).toBeGreaterThan(
tsconfigCopyIndex,
);
});
test("keeps local turbo prune output out of git and container contexts", () => {
const webPackage = JSON.parse(
readFileSync(join(root, "apps/web/package.json"), "utf8"),
) as { scripts?: Record<string, string> };
const gitignore = readFileSync(join(root, ".gitignore"), "utf8");
const containerignore = readFileSync(join(root, ".containerignore"), "utf8");
const containerIgnoreRules = new Set(
containerignore
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean),
);
expect(gitignore).toContain("out/");
expect(containerignore).toContain("out");
expect([...containerIgnoreRules]).toEqual(
expect.arrayContaining([
"apps/*/.turbo",
"apps/*/build",
"apps/*/data",
"apps/*/dist",
"apps/*/playwright-report",
"apps/*/test-results",
"packages/*/.turbo",
"packages/*/dist",
"packages/*/storybook-static",
]),
);
expect(webPackage.scripts?.build).toBe(
"rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build",
);
}); });
}); });