diff --git a/.containerignore b/.containerignore index 70b617e..b4b14d1 100644 --- a/.containerignore +++ b/.containerignore @@ -5,6 +5,7 @@ coverage data dist node_modules +out playwright-report storybook-static test-results diff --git a/.gitignore b/.gitignore index b6326bb..eed0de1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +out/ .svelte-kit/ build/ dist/ diff --git a/README.md b/README.md index 572f9bc..fee9443 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,11 @@ the checked-in `apps/web/drizzle/` directory so startup migrations can run. ### Internal Container -The checked-in `apps/web/Containerfile` builds the React client and Bun server -from the workspace root into a runtime image. For the Dimension Lab internal -host, run it behind Caddy on a loopback port and mount persistent state at -`/data`: +The checked-in `apps/web/Containerfile` runs +`turbo prune @dimensionlab/web --docker`, installs the pruned manifest set, and +builds the React client plus Bun server from the pruned workspace source. For the +Dimension Lab internal host, run it behind Caddy on a loopback port and mount +persistent state at `/data`: ```sh podman build -f apps/web/Containerfile -t localhost/dimensionlab-website:latest . diff --git a/apps/web/Containerfile b/apps/web/Containerfile index 7f12f94..9ec2baa 100644 --- a/apps/web/Containerfile +++ b/apps/web/Containerfile @@ -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 -ENV PATH=/repo/apps/web/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 -COPY packages/dashboard-model/package.json packages/dashboard-model/package.json -COPY packages/ui/package.json packages/ui/package.json +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 + +FROM base AS pruner + +COPY . . +RUN bunx turbo prune @dimensionlab/web --docker + +FROM base AS deps + +COPY --from=pruner /repo/out/json/ ./ RUN bun install --frozen-lockfile FROM deps AS build -COPY . . -RUN bun install --frozen-lockfile && bun run build +COPY --from=pruner /repo/out/full/ ./ +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 ENV NODE_ENV=production diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 4f4200c..9c056bf 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -114,35 +114,47 @@ describe("workspace boundaries", () => { ); }); - test("stages web workspace dependency manifests before container install", () => { - const webPackage = JSON.parse( - readFileSync(join(root, "apps/web/package.json"), "utf8"), - ) as { dependencies?: Record }; - 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); + test("builds the internal container from a turbo-pruned web workspace", () => { const containerfile = readFileSync( join(root, "apps/web/Containerfile"), "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( - webPackage.dependencies ?? {}, - ) - .filter(([, version]) => version.startsWith("workspace:")) - .map(([packageName]) => manifestsByPackageName.get(packageName)); + expect(containerfile).toContain("AS pruner"); + expect(containerfile).toContain(pruneCommand); + expect(containerfile).toContain(jsonCopy); + expect(containerfile).toContain(sourceCopy); + 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); - for (const manifestPath of workspaceDependencyManifests) { - expect(containerfile).toContain(`COPY ${manifestPath} ${manifestPath}`); - } + const jsonCopyIndex = containerfile.indexOf(jsonCopy); + const installIndex = containerfile.indexOf(installCommand); + 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 gitignore = readFileSync(join(root, ".gitignore"), "utf8"); + const containerignore = readFileSync(join(root, ".containerignore"), "utf8"); + + expect(gitignore).toContain("out/"); + expect(containerignore).toContain("out"); }); });