11#!/usr/bin/env node
2- ' use strict' ;
2+ " use strict" ;
33
44/**
55 * scripts/prepare-web.js
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
4141function 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
4646function 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
5661if ( ! 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
6570if ( ! 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" ) ;
7984const isNested = ! fs . existsSync ( flatServerJs ) && fs . existsSync ( nestedServerJs ) ;
8085
8186if ( ! 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
8994if ( 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
99104if ( 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.
111116if ( 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
120127const 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)
125132copyDir ( STATIC_SRC , staticDest ) ;
126133
127134// Copy public/ next to server.js
128135const 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
132139copyDir ( 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 ━━━" ) ;
135177console . log ( ` Output: ${ WEB_BUILD_DIR } \n` ) ;
0 commit comments