From b17bd7bb542aa4a4a03545a66d321e48c70eb1e2 Mon Sep 17 00:00:00 2001 From: Hannes Hauswedell Date: Thu, 16 Apr 2026 21:17:21 +0200 Subject: [PATCH 1/2] feature: FreeBSD support assisted-by: Claude Opus 4.6 --- package.json | 7 +-- scripts/next-runner.js | 17 ++++++ scripts/postinstall.js | 114 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 scripts/next-runner.js create mode 100644 scripts/postinstall.js diff --git a/package.json b/package.json index 00da247b..fbe35ef8 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "1.22.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build && node scripts/postbuild.js", + "dev": "node scripts/next-runner.js dev", + "build": "node scripts/next-runner.js build && node scripts/postbuild.js", "start": "next start", "lint": "eslint .", "test": "vitest", @@ -14,7 +14,8 @@ "mock:data:notes": "tsx tests/mock-data/note-generator.ts", "docker:test": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml build && docker compose -f docker-compose.test.yml up -d", "docker:test:logs": "docker compose -f docker-compose.test.yml logs -f", - "docker:test:restart": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml up -d" + "docker:test:restart": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml up -d", + "postinstall": "node scripts/postinstall.js" }, "dependencies": { "@dnd-kit/core": "6.3.1", diff --git a/scripts/next-runner.js b/scripts/next-runner.js new file mode 100644 index 00000000..ce2a1390 --- /dev/null +++ b/scripts/next-runner.js @@ -0,0 +1,17 @@ +'use strict'; + +/** + * Wrapper around `next` that appends --webpack on FreeBSD, where Turbopack is + * unavailable due to missing native bindings. On all other platforms the + * arguments are passed through unchanged, preserving Turbopack as the default. + */ + +const { spawnSync } = require('child_process'); + +const args = process.argv.slice(2); +if (process.platform === 'freebsd') { + args.push('--webpack'); +} + +const result = spawnSync('next', args, { stdio: 'inherit', shell: true }); +process.exit(result.status ?? 1); diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 00000000..8637958d --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,114 @@ +'use strict'; + +/** + * Patches @swc/core/binding.js on FreeBSD so that modules importing @swc/core + * do not crash at require time when no native binary is available. + * + * Context: next-intl (and @serwist/turbopack) depend on @swc/core, whose + * binding.js throws immediately on unsupported platforms. On FreeBSD there is + * no native binary and no published WASM fallback package, so the import fails + * before the build even starts. The patch makes binding.js return a lazy stub + * on FreeBSD instead — methods only throw when actually called, which is never + * the case in this project (next-intl's experimental.extract is not enabled). + */ + +const fs = require('fs'); +const path = require('path'); + +if (process.platform !== 'freebsd') { + process.exit(0); +} + +const NEEDLE = `if (!nativeBinding) { + if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }) + } + throw new Error(\`Failed to load native binding\`) +}`; + +const REPLACEMENT = `if (!nativeBinding) { + if (process.platform === 'freebsd') { + // FreeBSD: no native or WASM binary is available from @swc/core. + // Return a stub so modules that import @swc/core without using it can load. + // Any actual call to @swc/core methods will throw at call time. + const noBinding = () => { + throw new Error( + '@swc/core: no native or WASM binding available for FreeBSD. ' + + 'Build @swc/core from source or provide a WASM fallback.' + ) + } + nativeBinding = { + Compiler: class { constructor() { noBinding() } }, + JsCompiler: class { constructor() { noBinding() } }, + analyze: noBinding, bundle: noBinding, + getTargetTriple: () => 'x86_64-unknown-freebsd', + initCustomTraceSubscriber: noBinding, minify: noBinding, + minifySync: noBinding, newMangleNameCache: noBinding, + parse: noBinding, parseFile: noBinding, + parseFileSync: noBinding, parseSync: noBinding, + print: noBinding, printSync: noBinding, + transform: noBinding, transformFile: noBinding, + transformFileSync: noBinding, transformSync: noBinding, + } + } else if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }) + } else { + throw new Error(\`Failed to load native binding\`) + } +}`; + +// Find all @swc/core/binding.js files under node_modules (handles hoisted and +// nested installs, e.g. node_modules/@swc/core and +// node_modules/next-intl/node_modules/@swc/core). +function findBindingFiles(dir, results = []) { + const swcCore = path.join(dir, '@swc', 'core', 'binding.js'); + if (fs.existsSync(swcCore)) { + results.push(swcCore); + } + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + if (entry.name === '@swc') continue; // already checked above + const nested = path.join(dir, entry.name, 'node_modules'); + if (fs.existsSync(nested)) { + findBindingFiles(nested, results); + } + } + } catch (_) {} + return results; +} + +const nodeModules = path.join(__dirname, '..', 'node_modules'); +const bindingFiles = findBindingFiles(nodeModules); + +if (bindingFiles.length === 0) { + console.warn('postinstall: no @swc/core/binding.js found — skipping FreeBSD patch.'); + process.exit(0); +} + +let patched = 0; +for (const file of bindingFiles) { + const src = fs.readFileSync(file, 'utf8'); + if (src.includes('x86_64-unknown-freebsd')) { + continue; // already patched + } + if (!src.includes(NEEDLE)) { + console.warn(`postinstall: unexpected binding.js format at ${file} — skipping.`); + continue; + } + fs.writeFileSync(file, src.replace(NEEDLE, REPLACEMENT)); + patched++; + console.log(`postinstall: patched ${file}`); +} + +if (patched === 0) { + console.log('postinstall: @swc/core/binding.js already patched, nothing to do.'); +} From 31a05de1aa07028ffc598785b770a33aa9d7a70c Mon Sep 17 00:00:00 2001 From: fccview Date: Mon, 27 Apr 2026 09:44:26 +0100 Subject: [PATCH 2/2] change this to work with the new patching system instead --- howto/ENV-VARIABLES.md | 1 + howto/PATCHES.md | 18 ++++++ package.json | 7 +-- patches/freebsd_20260427.js | 110 ++++++++++++++++++++++++++++++++++ scripts/next-runner.js | 17 ------ scripts/postinstall.js | 114 ------------------------------------ 6 files changed, 132 insertions(+), 135 deletions(-) create mode 100644 patches/freebsd_20260427.js delete mode 100644 scripts/next-runner.js delete mode 100644 scripts/postinstall.js diff --git a/howto/ENV-VARIABLES.md b/howto/ENV-VARIABLES.md index 6a7431ea..26eaa952 100644 --- a/howto/ENV-VARIABLES.md +++ b/howto/ENV-VARIABLES.md @@ -38,6 +38,7 @@ OIDC_ADMIN_GROUPS=admins - `DISABLE_BRUTEFORCE_PROTECTION=yes` Optional. Disables brute force protection for local login authentication. By default, accounts are temporarily locked after 3 failed login attempts with exponential delays (10s, 30s, 60s, etc.). Set to `yes` to completely disable this security feature. - `ENABLE_PWA_ZOOM=yes` Optional. Enables zoomming on the PWA for accessibility reasons. - `JOTTY_BODY_SIZE_LIMIT=100mb` Optional. Maximum request body size accepted by Server Actions (uploads, drawio attachments, avatars, etc.). Defaults to `100mb`. Accepts `b`, `kb`, `mb`, `gb` (e.g. `50mb`, `2gb`). Applied at container start via the runtime patcher — see [Runtime Patches](./PATCHES.md). +- `JOTTY_FREEBSD=1` Optional. **FreeBSD only.** Enables the FreeBSD compatibility patch which stubs `@swc/core` (no native or WASM binary is published for FreeBSD) and forces Next.js to use webpack instead of Turbopack. Has no effect on Linux/macOS/Windows — leave unset everywhere else. Applied at container start via the runtime patcher — see [Runtime Patches](./PATCHES.md). ## SSO Configuration (Optional) diff --git a/howto/PATCHES.md b/howto/PATCHES.md index b5549b60..af674a5d 100644 --- a/howto/PATCHES.md +++ b/howto/PATCHES.md @@ -55,3 +55,21 @@ environment: ``` + +
+freebsd_20260427.js — FreeBSD compatibility (stub @swc/core, force webpack) + +FreeBSD has no prebuilt native binary for `@swc/core` and no published WASM fallback, so any module that imports it (next-intl, @serwist/turbopack) crashes at require time. Turbopack is also unavailable for the same reason. This patch: + +1. Stubs `node_modules/@swc/core/binding.js` (hoisted + nested copies) so imports succeed. Stub methods only throw when actually called — which never happens in this project. +2. Patches `next/dist/lib/bundler.js` `parseBundlerArgs()` to force the webpack bundler, so Next never tries to load Turbopack. + +- **Gated on:** `JOTTY_FREEBSD` env var. Without it, the patch is a strict no-op — nothing under `node_modules` is read or modified. +- **Default:** disabled (Linux/macOS/Windows users should leave it unset). + +```yaml +environment: + - JOTTY_FREEBSD=1 +``` + +
diff --git a/package.json b/package.json index fbe35ef8..00da247b 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "1.22.0", "private": true, "scripts": { - "dev": "node scripts/next-runner.js dev", - "build": "node scripts/next-runner.js build && node scripts/postbuild.js", + "dev": "next dev", + "build": "next build && node scripts/postbuild.js", "start": "next start", "lint": "eslint .", "test": "vitest", @@ -14,8 +14,7 @@ "mock:data:notes": "tsx tests/mock-data/note-generator.ts", "docker:test": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml build && docker compose -f docker-compose.test.yml up -d", "docker:test:logs": "docker compose -f docker-compose.test.yml logs -f", - "docker:test:restart": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml up -d", - "postinstall": "node scripts/postinstall.js" + "docker:test:restart": "docker compose -f docker-compose.test.yml down && docker compose -f docker-compose.test.yml up -d" }, "dependencies": { "@dnd-kit/core": "6.3.1", diff --git a/patches/freebsd_20260427.js b/patches/freebsd_20260427.js new file mode 100644 index 00000000..d5e1f170 --- /dev/null +++ b/patches/freebsd_20260427.js @@ -0,0 +1,110 @@ +/** + * FreeBSD Compatibility Patch (2026-04-27) + * + * FreeBSD ships without prebuilt native binaries for @swc/core and Turbopack, + * so a stock `next dev`/`next build` crashes at require time. This patch: + * + * 1. Stubs node_modules/@swc/core/binding.js so modules that import @swc/core + * (next-intl, @serwist/turbopack) do not throw at load. Stub methods only + * throw when actually called — which never happens in this project. + * 2. Forces the Next.js bundler selector to webpack by injecting a guard at + * the top of node_modules/next/dist/lib/bundler.js parseBundlerArgs(), + * so Turbopack (which has no FreeBSD binary) is never chosen. + * + * Gated on the JOTTY_FREEBSD env var: this patch is a no-op everywhere else. + * Idempotent: re-runs detect prior application and skip. + */ + +const fs = require("fs"); +const path = require("path"); + +const _SWC_NEEDLE = `if (!nativeBinding) { + if (loadErrors.length > 0) { + // TODO Link to documentation with potential fixes + // - The package owner could build/publish bindings for this arch + // - The user may need to bundle the correct files + // - The user may need to re-install node_modules to get new packages + throw new Error('Failed to load native binding', { cause: loadErrors }) + } + throw new Error(\`Failed to load native binding\`) +}`; + +const _SWC_REPLACEMENT = `if (!nativeBinding) { + if (process.env.JOTTY_FREEBSD) { + const noBinding = () => { + throw new Error( + '@swc/core: no native or WASM binding available for FreeBSD. ' + + 'Build @swc/core from source or provide a WASM fallback.' + ) + } + nativeBinding = { + Compiler: class { constructor() { noBinding() } }, + JsCompiler: class { constructor() { noBinding() } }, + analyze: noBinding, bundle: noBinding, + getTargetTriple: () => 'x86_64-unknown-freebsd', + initCustomTraceSubscriber: noBinding, minify: noBinding, + minifySync: noBinding, newMangleNameCache: noBinding, + parse: noBinding, parseFile: noBinding, + parseFileSync: noBinding, parseSync: noBinding, + print: noBinding, printSync: noBinding, + transform: noBinding, transformFile: noBinding, + transformFileSync: noBinding, transformSync: noBinding, + } + } else if (loadErrors.length > 0) { + throw new Error('Failed to load native binding', { cause: loadErrors }) + } else { + throw new Error(\`Failed to load native binding\`) + } +}`; + +const _BUNDLER_NEEDLE = `function parseBundlerArgs(options) {`; +const _BUNDLER_REPLACEMENT = `function parseBundlerArgs(options) { + if (process.env.JOTTY_FREEBSD) { options.webpack = true; options.turbopack = false; options.turbo = false; }`; +const _BUNDLER_MARKER = `JOTTY_FREEBSD`; + +const _findSwcBindings = (dir, results = []) => { + const swcCore = path.join(dir, "@swc", "core", "binding.js"); + if (fs.existsSync(swcCore)) results.push(swcCore); + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === "@swc") continue; + const nested = path.join(dir, entry.name, "node_modules"); + if (fs.existsSync(nested)) _findSwcBindings(nested, results); + } + } catch (_) { } + return results; +}; + +const _patchSwc = (file) => { + const src = fs.readFileSync(file, "utf8"); + if (src.includes("x86_64-unknown-freebsd")) return "noop"; + if (!src.includes(_SWC_NEEDLE)) return "skipped"; + fs.writeFileSync(file, src.replace(_SWC_NEEDLE, _SWC_REPLACEMENT)); + return "patched"; +}; + +const _patchBundler = (file) => { + if (!fs.existsSync(file)) return "missing"; + const src = fs.readFileSync(file, "utf8"); + if (src.includes(_BUNDLER_MARKER)) return "noop"; + if (!src.includes(_BUNDLER_NEEDLE)) return "skipped"; + fs.writeFileSync(file, src.replace(_BUNDLER_NEEDLE, _BUNDLER_REPLACEMENT)); + return "patched"; +}; + +module.exports = { + name: "freebsd_20260427", + apply: (ctx) => { + if (!process.env.JOTTY_FREEBSD) return "skipped (JOTTY_FREEBSD not set)"; + + const counts = { patched: 0, noop: 0, skipped: 0, missing: 0 }; + const nodeModules = path.join(ctx.projectRoot, "node_modules"); + + for (const file of _findSwcBindings(nodeModules)) { + counts[_patchSwc(file)]++; + } + counts[_patchBundler(path.join(nodeModules, "next", "dist", "lib", "bundler.js"))]++; + + return `swc+bundler patched=${counts.patched}, noop=${counts.noop}, skipped=${counts.skipped}, missing=${counts.missing}`; + }, +}; diff --git a/scripts/next-runner.js b/scripts/next-runner.js deleted file mode 100644 index ce2a1390..00000000 --- a/scripts/next-runner.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -/** - * Wrapper around `next` that appends --webpack on FreeBSD, where Turbopack is - * unavailable due to missing native bindings. On all other platforms the - * arguments are passed through unchanged, preserving Turbopack as the default. - */ - -const { spawnSync } = require('child_process'); - -const args = process.argv.slice(2); -if (process.platform === 'freebsd') { - args.push('--webpack'); -} - -const result = spawnSync('next', args, { stdio: 'inherit', shell: true }); -process.exit(result.status ?? 1); diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100644 index 8637958d..00000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -/** - * Patches @swc/core/binding.js on FreeBSD so that modules importing @swc/core - * do not crash at require time when no native binary is available. - * - * Context: next-intl (and @serwist/turbopack) depend on @swc/core, whose - * binding.js throws immediately on unsupported platforms. On FreeBSD there is - * no native binary and no published WASM fallback package, so the import fails - * before the build even starts. The patch makes binding.js return a lazy stub - * on FreeBSD instead — methods only throw when actually called, which is never - * the case in this project (next-intl's experimental.extract is not enabled). - */ - -const fs = require('fs'); -const path = require('path'); - -if (process.platform !== 'freebsd') { - process.exit(0); -} - -const NEEDLE = `if (!nativeBinding) { - if (loadErrors.length > 0) { - // TODO Link to documentation with potential fixes - // - The package owner could build/publish bindings for this arch - // - The user may need to bundle the correct files - // - The user may need to re-install node_modules to get new packages - throw new Error('Failed to load native binding', { cause: loadErrors }) - } - throw new Error(\`Failed to load native binding\`) -}`; - -const REPLACEMENT = `if (!nativeBinding) { - if (process.platform === 'freebsd') { - // FreeBSD: no native or WASM binary is available from @swc/core. - // Return a stub so modules that import @swc/core without using it can load. - // Any actual call to @swc/core methods will throw at call time. - const noBinding = () => { - throw new Error( - '@swc/core: no native or WASM binding available for FreeBSD. ' + - 'Build @swc/core from source or provide a WASM fallback.' - ) - } - nativeBinding = { - Compiler: class { constructor() { noBinding() } }, - JsCompiler: class { constructor() { noBinding() } }, - analyze: noBinding, bundle: noBinding, - getTargetTriple: () => 'x86_64-unknown-freebsd', - initCustomTraceSubscriber: noBinding, minify: noBinding, - minifySync: noBinding, newMangleNameCache: noBinding, - parse: noBinding, parseFile: noBinding, - parseFileSync: noBinding, parseSync: noBinding, - print: noBinding, printSync: noBinding, - transform: noBinding, transformFile: noBinding, - transformFileSync: noBinding, transformSync: noBinding, - } - } else if (loadErrors.length > 0) { - // TODO Link to documentation with potential fixes - // - The package owner could build/publish bindings for this arch - // - The user may need to bundle the correct files - // - The user may need to re-install node_modules to get new packages - throw new Error('Failed to load native binding', { cause: loadErrors }) - } else { - throw new Error(\`Failed to load native binding\`) - } -}`; - -// Find all @swc/core/binding.js files under node_modules (handles hoisted and -// nested installs, e.g. node_modules/@swc/core and -// node_modules/next-intl/node_modules/@swc/core). -function findBindingFiles(dir, results = []) { - const swcCore = path.join(dir, '@swc', 'core', 'binding.js'); - if (fs.existsSync(swcCore)) { - results.push(swcCore); - } - try { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - if (entry.name === '@swc') continue; // already checked above - const nested = path.join(dir, entry.name, 'node_modules'); - if (fs.existsSync(nested)) { - findBindingFiles(nested, results); - } - } - } catch (_) {} - return results; -} - -const nodeModules = path.join(__dirname, '..', 'node_modules'); -const bindingFiles = findBindingFiles(nodeModules); - -if (bindingFiles.length === 0) { - console.warn('postinstall: no @swc/core/binding.js found — skipping FreeBSD patch.'); - process.exit(0); -} - -let patched = 0; -for (const file of bindingFiles) { - const src = fs.readFileSync(file, 'utf8'); - if (src.includes('x86_64-unknown-freebsd')) { - continue; // already patched - } - if (!src.includes(NEEDLE)) { - console.warn(`postinstall: unexpected binding.js format at ${file} — skipping.`); - continue; - } - fs.writeFileSync(file, src.replace(NEEDLE, REPLACEMENT)); - patched++; - console.log(`postinstall: patched ${file}`); -} - -if (patched === 0) { - console.log('postinstall: @swc/core/binding.js already patched, nothing to do.'); -}