Skip to content

Commit d5e03a0

Browse files
committed
fix: resolve MODULE_NOT_FOUND in production builds
- Dereference pnpm symlinks when copying standalone output (dereference: true) Fixes broken absolute symlinks pointing to CI build paths - Patch incomplete packages (node-stdlib-browser, esbuild) that Next.js file tracing misses due to dynamic require.resolve() calls - Bump version to 1.0.5
1 parent 840c0c1 commit d5e03a0

File tree

4 files changed

+117
-66
lines changed

4 files changed

+117
-66
lines changed

overlay/next.config.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@ import path from "path";
22
import type { NextConfig } from "next";
33

44
const nextConfig: NextConfig = {
5-
// Required for the desktop app: produces a self-contained server in .next/standalone/
6-
// This has no effect on normal web deployments (Railway, Vercel, etc.).
7-
output: "standalone",
8-
// Allow hot-reload WebSocket connections from 127.0.0.1 (Electron dev mode).
9-
// Safe: this only applies in development; has no effect in production builds.
10-
allowedDevOrigins: ["127.0.0.1"],
11-
serverExternalPackages: ["just-bash", "bash-tool", "node-liblzma", "@mongodb-js/zstd", "@secure-exec/node", "@secure-exec/core", "isolated-vm", "esbuild"],
12-
// Set the Turbopack workspace root to desktop/ (one level above web/).
13-
// With pnpm workspaces, all node_modules live in desktop/node_modules/ and
14-
// web/node_modules/ contains symlinks pointing up to desktop/node_modules/.pnpm/.
15-
// Turbopack must have its root set to desktop/ so it can follow those symlinks —
16-
// it refuses to access files outside its root for security reasons.
17-
turbopack: {
18-
root: path.resolve(__dirname, ".."),
19-
},
5+
// Required for the desktop app: produces a self-contained server in .next/standalone/
6+
// This has no effect on normal web deployments (Railway, Vercel, etc.).
7+
output: "standalone",
8+
// Allow hot-reload WebSocket connections from 127.0.0.1 (Electron dev mode).
9+
// Safe: this only applies in development; has no effect in production builds.
10+
allowedDevOrigins: ["127.0.0.1"],
11+
serverExternalPackages: [
12+
"just-bash",
13+
"bash-tool",
14+
"node-liblzma",
15+
"@mongodb-js/zstd",
16+
"@secure-exec/node",
17+
"@secure-exec/core",
18+
"isolated-vm",
19+
"esbuild",
20+
],
21+
// Set the Turbopack workspace root to desktop/ (one level above web/).
22+
// With pnpm workspaces, all node_modules live in desktop/node_modules/ and
23+
// web/node_modules/ contains symlinks pointing up to desktop/node_modules/.pnpm/.
24+
// Turbopack must have its root set to desktop/ so it can follow those symlinks —
25+
// it refuses to access files outside its root for security reasons.
26+
turbopack: {
27+
root: path.resolve(__dirname, ".."),
28+
},
2029
};
2130

2231
export default nextConfig;

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "infinite-monitor-desktop",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "Desktop application for Infinite Monitor – AI-powered dashboard builder",
55
"main": "electron/main.js",
66
"author": {

scripts/prepare-web.js

Lines changed: 90 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env node
2-
'use strict';
2+
"use strict";
33

44
/**
55
* scripts/prepare-web.js
@@ -25,79 +25,84 @@
2525
* node_modules/ ← bundled server-side node_modules (from standalone)
2626
*/
2727

28-
const { execSync } = require('child_process');
29-
const path = require('path');
30-
const fs = require('fs');
28+
const { execSync } = require("child_process");
29+
const path = require("path");
30+
const fs = require("fs");
3131

32-
const ROOT = path.resolve(__dirname, '..');
33-
const WEB_DIR = path.join(ROOT, 'web');
34-
const STANDALONE_DIR = path.join(WEB_DIR, '.next', 'standalone');
35-
const STATIC_SRC = path.join(WEB_DIR, '.next', 'static');
36-
const PUBLIC_SRC = path.join(WEB_DIR, 'public');
37-
const WEB_BUILD_DIR = path.join(ROOT, 'web-build');
32+
const ROOT = path.resolve(__dirname, "..");
33+
const WEB_DIR = path.join(ROOT, "web");
34+
const STANDALONE_DIR = path.join(WEB_DIR, ".next", "standalone");
35+
const STATIC_SRC = path.join(WEB_DIR, ".next", "static");
36+
const PUBLIC_SRC = path.join(WEB_DIR, "public");
37+
const WEB_BUILD_DIR = path.join(ROOT, "web-build");
3838

3939
// ── Helpers ──────────────────────────────────────────────────────────────────
4040

4141
function run(cmd, cwd) {
42-
console.log(`\n $ ${cmd} (in ${path.relative(process.cwd(), cwd)})`);
43-
execSync(cmd, { cwd, stdio: 'inherit' });
42+
console.log(`\n $ ${cmd} (in ${path.relative(process.cwd(), cwd)})`);
43+
execSync(cmd, { cwd, stdio: "inherit" });
4444
}
4545

4646
function copyDir(src, dest) {
47-
if (!fs.existsSync(src)) return;
48-
fs.cpSync(src, dest, { recursive: true });
49-
console.log(` copied: ${path.relative(ROOT, src)}${path.relative(ROOT, dest)}`);
47+
if (!fs.existsSync(src)) return;
48+
// dereference: true resolves all symlinks to real files/directories.
49+
// Critical for pnpm workspaces where node_modules contain absolute symlinks
50+
// pointing to the build machine's filesystem — these break on other machines.
51+
fs.cpSync(src, dest, { recursive: true, dereference: true });
52+
console.log(
53+
` copied: ${path.relative(ROOT, src)}${path.relative(ROOT, dest)}`,
54+
);
5055
}
5156

5257
// ── Main ─────────────────────────────────────────────────────────────────────
5358

54-
console.log('\n━━━ prepare-web: building upstream Next.js app ━━━\n');
59+
console.log("\n━━━ prepare-web: building upstream Next.js app ━━━\n");
5560

5661
if (!fs.existsSync(WEB_DIR)) {
57-
console.error(`ERROR: web directory not found at:\n ${WEB_DIR}`);
58-
process.exit(1);
62+
console.error(`ERROR: web directory not found at:\n ${WEB_DIR}`);
63+
process.exit(1);
5964
}
6065

6166
// Step 1: build
62-
run('pnpm run build', WEB_DIR);
67+
run("pnpm run build", WEB_DIR);
6368

6469
// Verify standalone output was produced
6570
if (!fs.existsSync(STANDALONE_DIR)) {
66-
console.error(
67-
`\nERROR: .next/standalone was not produced by the build.\n` +
68-
`Make sure next.config.ts includes output: 'standalone'\n` +
69-
` (see desktop/ARCHITECTURE.md for details)`
70-
);
71-
process.exit(1);
71+
console.error(
72+
`\nERROR: .next/standalone was not produced by the build.\n` +
73+
`Make sure next.config.ts includes output: 'standalone'\n` +
74+
` (see desktop/ARCHITECTURE.md for details)`,
75+
);
76+
process.exit(1);
7277
}
7378

7479
// Detect standalone layout: flat (server.js at root) vs nested (server.js under web/)
7580
// The nested layout occurs when outputFileTracingRoot is set to the workspace root (desktop/)
7681
// instead of the project root (web/). We prefer flat, but handle both.
77-
const flatServerJs = path.join(STANDALONE_DIR, 'server.js');
78-
const nestedServerJs = path.join(STANDALONE_DIR, 'web', 'server.js');
82+
const flatServerJs = path.join(STANDALONE_DIR, "server.js");
83+
const nestedServerJs = path.join(STANDALONE_DIR, "web", "server.js");
7984
const isNested = !fs.existsSync(flatServerJs) && fs.existsSync(nestedServerJs);
8085

8186
if (!fs.existsSync(flatServerJs) && !fs.existsSync(nestedServerJs)) {
82-
console.error(
83-
`\nERROR: server.js not found in standalone output.\n` +
84-
`Checked:\n ${flatServerJs}\n ${nestedServerJs}`
85-
);
86-
process.exit(1);
87+
console.error(
88+
`\nERROR: server.js not found in standalone output.\n` +
89+
`Checked:\n ${flatServerJs}\n ${nestedServerJs}`,
90+
);
91+
process.exit(1);
8792
}
8893

8994
if (isNested) {
90-
console.warn(
91-
'\n WARNING: standalone output uses nested layout (web/server.js).\n' +
92-
' Add outputFileTracingRoot: path.resolve(__dirname) to next.config.ts for a flat layout.\n'
93-
);
95+
console.warn(
96+
"\n WARNING: standalone output uses nested layout (web/server.js).\n" +
97+
" Add outputFileTracingRoot: path.resolve(__dirname) to next.config.ts for a flat layout.\n",
98+
);
9499
}
95100

96101
// Step 2: assemble web-build/
97-
console.log('\n━━━ prepare-web: assembling web-build/ ━━━\n');
102+
console.log("\n━━━ prepare-web: assembling web-build/ ━━━\n");
98103

99104
if (fs.existsSync(WEB_BUILD_DIR)) {
100-
fs.rmSync(WEB_BUILD_DIR, { recursive: true, force: true });
105+
fs.rmSync(WEB_BUILD_DIR, { recursive: true, force: true });
101106
}
102107

103108
// Copy the entire standalone directory (includes server.js + node_modules)
@@ -109,27 +114,64 @@ copyDir(STANDALONE_DIR, WEB_BUILD_DIR);
109114
// MODULE_NOT_FOUND errors because Node resolves them before the real standalone
110115
// node_modules/ at the parent level. Remove the broken node_modules directory.
111116
if (isNested) {
112-
const brokenNodeModules = path.join(WEB_BUILD_DIR, 'web', 'node_modules');
113-
if (fs.existsSync(brokenNodeModules)) {
114-
fs.rmSync(brokenNodeModules, { recursive: true, force: true });
115-
console.log(' removed: web-build/web/node_modules (broken pnpm workspace symlinks)');
116-
}
117+
const brokenNodeModules = path.join(WEB_BUILD_DIR, "web", "node_modules");
118+
if (fs.existsSync(brokenNodeModules)) {
119+
fs.rmSync(brokenNodeModules, { recursive: true, force: true });
120+
console.log(
121+
" removed: web-build/web/node_modules (broken pnpm workspace symlinks)",
122+
);
123+
}
117124
}
118125

119126
// Determine where server.js landed in web-build and copy static alongside it
120127
const staticDest = isNested
121-
? path.join(WEB_BUILD_DIR, 'web', '.next', 'static')
122-
: path.join(WEB_BUILD_DIR, '.next', 'static');
128+
? path.join(WEB_BUILD_DIR, "web", ".next", "static")
129+
: path.join(WEB_BUILD_DIR, ".next", "static");
123130

124131
// Copy .next/static next to server.js (required by the standalone server)
125132
copyDir(STATIC_SRC, staticDest);
126133

127134
// Copy public/ next to server.js
128135
const publicDest = isNested
129-
? path.join(WEB_BUILD_DIR, 'web', 'public')
130-
: path.join(WEB_BUILD_DIR, 'public');
136+
? path.join(WEB_BUILD_DIR, "web", "public")
137+
: path.join(WEB_BUILD_DIR, "public");
131138

132139
copyDir(PUBLIC_SRC, publicDest);
133140

134-
console.log('\n━━━ prepare-web: done ━━━');
141+
// Step 3: patch incomplete packages
142+
// Next.js file tracing may miss files loaded dynamically via require.resolve().
143+
// node-stdlib-browser's esm/index.js uses resolvePath('./mock/empty.js') which
144+
// the tracer cannot follow. Copy the full package from the source node_modules.
145+
const buildNodeModules = path.join(WEB_BUILD_DIR, "node_modules");
146+
const patchPackages = ["node-stdlib-browser", "esbuild"];
147+
for (const pkg of patchPackages) {
148+
const dest = path.join(buildNodeModules, pkg);
149+
if (!fs.existsSync(dest)) continue; // not in the build, skip
150+
// Find the full package in the pnpm store or node_modules
151+
const pnpmStore = path.join(ROOT, "node_modules", ".pnpm");
152+
let fullPkgSrc = null;
153+
if (fs.existsSync(pnpmStore)) {
154+
// Search the pnpm virtual store for the package
155+
for (const entry of fs.readdirSync(pnpmStore)) {
156+
if (!entry.startsWith(pkg.replace("/", "+") + "@")) continue;
157+
const candidate = path.join(pnpmStore, entry, "node_modules", pkg);
158+
if (fs.existsSync(candidate)) {
159+
fullPkgSrc = candidate;
160+
break;
161+
}
162+
}
163+
}
164+
if (!fullPkgSrc) {
165+
// Fallback: try direct node_modules path
166+
const direct = path.join(ROOT, "node_modules", pkg);
167+
if (fs.existsSync(direct)) fullPkgSrc = direct;
168+
}
169+
if (fullPkgSrc) {
170+
fs.rmSync(dest, { recursive: true, force: true });
171+
fs.cpSync(fullPkgSrc, dest, { recursive: true, dereference: true });
172+
console.log(` patched: ${pkg} (copied full package for dynamic requires)`);
173+
}
174+
}
175+
176+
console.log("\n━━━ prepare-web: done ━━━");
135177
console.log(` Output: ${WEB_BUILD_DIR}\n`);

0 commit comments

Comments
 (0)