diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 0de5b00..3145bbb 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -57,6 +57,11 @@ Drizzle Kit, npm installs, or a compiler at runtime. Startup applies the committ SQL migrations and seeds samples only when the database is empty. `/api/health` checks database readiness without rendering the portfolio. +Sharp uses native image processing where supported and includes a WebAssembly +fallback for servers without the required x86-64 CPU features. The fallback +preserves uploads and image optimization, but can use more CPU time. Keep +`@img/sharp-wasm32` at the same version as `sharp` when upgrading. + Use one application instance with a local filesystem. Do not mount the SQLite database over NFS/SMB, use an ephemeral serverless disk, or run multiple replicas against the same data volume. PostgreSQL would be the next step if those become diff --git a/next.config.mjs b/next.config.mjs index 70d8f86..c8bca10 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,7 +3,12 @@ const nextConfig = { output: 'standalone', serverExternalPackages: ['better-sqlite3'], outputFileTracingIncludes: { - '/*': ['./migrations/**/*'], + '/*': [ + './migrations/**/*', + './node_modules/@img/sharp-wasm32/**/*', + './node_modules/@emnapi/runtime/**/*', + './node_modules/tslib/**/*', + ], }, outputFileTracingExcludes: { '*': ['./data/**/*', './backups/**/*', './**/auth-secret', './**/*.sqlite*', './.env*'], diff --git a/package-lock.json b/package-lock.json index 5c2b1e4..0a72c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "portfolio-v2-antigravity", "version": "0.1.0", "dependencies": { + "@img/sharp-wasm32": "0.35.4", "better-auth": "^1.7.5", "better-sqlite3": "^13.0.3", "drizzle-orm": "^0.45.2", @@ -456,7 +457,6 @@ "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@emnapi/runtime/-/runtime-1.11.3.tgz", "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", - "optional": true, "dependencies": { "tslib": "^2.4.0" } @@ -1610,7 +1610,6 @@ "resolved": "https://package-mirror.liara.ir/artifactory/api/npm/npm/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, "dependencies": { "@emnapi/runtime": "^1.11.3" }, diff --git a/package.json b/package.json index 0fddc75..4533c0a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "maintenance": "tsx --env-file-if-exists=.env scripts/maintenance.ts" }, "dependencies": { + "@img/sharp-wasm32": "0.35.4", "better-auth": "^1.7.5", "better-sqlite3": "^13.0.3", "drizzle-orm": "^0.45.2", diff --git a/tests/sharp-fallback.test.mjs b/tests/sharp-fallback.test.mjs new file mode 100644 index 0000000..1c99ada --- /dev/null +++ b/tests/sharp-fallback.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; + +test('image processing falls back to WebAssembly without native CPU support', () => { + // Isolate the native-binding simulation from the rest of the test process. + const result = spawnSync(process.execPath, ['-e', ` + const Module = require('node:module'); + const original = Module._load; + Module._load = function (id, ...args) { + if (id.endsWith('.node') && !id.includes('wasm32')) { + const error = new Error('Simulated unavailable native binding'); + error.code = 'MODULE_NOT_FOUND'; + throw error; + } + return original.call(this, id, ...args); + }; + const sharp = require('sharp'); + (async () => { + const image = await sharp({ create: { width: 8, height: 8, channels: 3, background: '#336699' } }).webp().toBuffer(); + const metadata = await sharp(image).metadata(); + if (metadata.format !== 'webp' || metadata.width !== 8 || metadata.height !== 8) throw new Error('Image round trip failed'); + })().catch(error => { console.error(error); process.exitCode = 1; }); + `], { encoding: 'utf8', timeout: 30000 }); + assert.equal(result.status, 0, result.stderr || result.error?.message); +});