From 1e9afb733a1b6579375ac49e0ff59cfad34bf2bc Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:19:35 +0100 Subject: [PATCH] fix: let the Docker image build see the script its Next config imports --- .dockerignore | 1 + lib/__tests__/dockerignore-scripts.test.ts | 48 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 lib/__tests__/dockerignore-scripts.test.ts diff --git a/.dockerignore b/.dockerignore index 060be0c1f..da9e55a3a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,7 @@ lite-image !.env.dev.example scripts/ !scripts/lite/ +!scripts/desktop-build-flag.mjs !scripts/build-plugin-sandbox.mjs !scripts/build-pst-worker.mjs !scripts/build-store-worker.mjs diff --git a/lib/__tests__/dockerignore-scripts.test.ts b/lib/__tests__/dockerignore-scripts.test.ts new file mode 100644 index 000000000..b2a185bef --- /dev/null +++ b/lib/__tests__/dockerignore-scripts.test.ts @@ -0,0 +1,48 @@ +import { existsSync, readFileSync } from 'fs'; +import { dirname, join, relative, resolve } from 'path'; +import { describe, expect, it } from 'vitest'; + +const ROOT = resolve(__dirname, '../..'); + +/** The scripts the image build can see: .dockerignore drops scripts/ and lists them one by one. */ +function allowedScripts(): string[] { + return readFileSync(join(ROOT, '.dockerignore'), 'utf-8') + .split(/\r?\n/) + .filter((line) => line.startsWith('!scripts/')) + .map((line) => line.slice(1)); +} + +function relativeImports(file: string): string[] { + const source = readFileSync(join(ROOT, file), 'utf-8'); + const found = source.matchAll(/(?:from\s+|import\s*\(\s*|require\(\s*|^import\s+)['"](\.{1,2}\/[^'"]+)['"]/gm); + return [...found] + .map((match) => relative(ROOT, resolve(ROOT, dirname(file), match[1])).replace(/\\/g, '/')) + .filter((target) => existsSync(join(ROOT, target))); +} + +/** Every file under scripts/ reachable from what the Dockerfiles run. */ +function scriptsNeededBy(entries: string[]): Set { + const seen = new Set(); + const queue = [...entries]; + while (queue.length > 0) { + const file = queue.pop()!; + if (seen.has(file) || !existsSync(join(ROOT, file))) continue; + seen.add(file); + if (/\.(mjs|cjs|js|ts)$/.test(file)) queue.push(...relativeImports(file)); + } + return new Set([...seen].filter((file) => file.startsWith('scripts/'))); +} + +describe('.dockerignore', () => { + it('lets through every script the image build imports', () => { + const allowed = allowedScripts(); + const covered = (file: string) => allowed.some((entry) => (entry.endsWith('/') ? file.startsWith(entry) : file === entry)); + const entries = ['next.config.ts', ...allowed.filter((entry) => !entry.endsWith('/'))]; + const missing = [...scriptsNeededBy(entries)].filter((file) => !covered(file)); + expect(missing).toEqual([]); + }); + + it('sees the scripts next.config.ts imports', () => { + expect(scriptsNeededBy(['next.config.ts'])).toContain('scripts/standalone-guard.mjs'); + }); +});