1 #!/bin/bash
2 # Copy frontend files from backup to artifact and handle Tailwind/theme setup.
3 # Usage: bash fullstack_copy_frontend.sh <app-name> [--client-dir <dir>] [--tw-version v3|v4] [--has-theme]
4 #
5 # Run AFTER createArtifact has created the artifact directory.
6 # If flags are omitted, auto-detects from .migration-backup/.
7
8 set -euo pipefail
9
10 BACKUP=".migration-backup"
11
12 if [ $# -lt 1 ]; then
13 echo "Usage: bash fullstack_copy_frontend.sh <app-name> [--client-dir <dir>] [--tw-version v3|v4] [--has-theme]"
14 exit 1
15 fi
16
17 APP_NAME="$1"; shift
18 CLIENT_DIR=""
19 TW_VERSION=""
20 HAS_THEME="false"
21
22 while [[ $# -gt 0 ]]; do
23 case "$1" in
24 --client-dir) CLIENT_DIR="$2"; shift 2 ;;
25 --tw-version) TW_VERSION="$2"; shift 2 ;;
26 --has-theme) HAS_THEME="true"; shift ;;
27 *) echo "Unknown arg: $1"; exit 1 ;;
28 esac
29 done
30
31 # --- Auto-discover client dir ---
32 if [ -z "$CLIENT_DIR" ]; then
33 if [ -d "$BACKUP/client/src" ]; then
34 CLIENT_DIR="client"
35 elif [ -d "$BACKUP/src/components" ] || [ -d "$BACKUP/src/pages" ] || [ -f "$BACKUP/src/App.tsx" ] || [ -f "$BACKUP/src/main.tsx" ]; then
36 CLIENT_DIR="src"
37 else
38 echo "ERROR: Could not find client directory. Pass --client-dir explicitly."
39 exit 1
40 fi
41 fi
42
43 # --- Auto-detect Tailwind version ---
44 if [ -z "$TW_VERSION" ]; then
45 TW_VERSION="v4"
46 CSS_FILE="$BACKUP/$CLIENT_DIR/src/index.css"
47 [ "$CLIENT_DIR" = "src" ] && CSS_FILE="$BACKUP/src/index.css"
48 if [ -f "$CSS_FILE" ] && grep -q '@tailwind base' "$CSS_FILE" 2>/dev/null; then
49 TW_VERSION="v3"
50 fi
51 fi
52
53 # --- Auto-detect theme.json ---
54 if [ "$HAS_THEME" = "false" ] && [ -f "$BACKUP/theme.json" ]; then
55 HAS_THEME="true"
56 fi
57
58 ARTIFACT_DIR="artifacts/$APP_NAME"
59
60 if [ ! -d "$ARTIFACT_DIR" ]; then
61 echo "ERROR: $ARTIFACT_DIR does not exist. Run createArtifact first."
62 exit 1
63 fi
64
65 echo "==== FRONTEND COPY ===="
66 echo "App: $APP_NAME | Client: $CLIENT_DIR | Tailwind: $TW_VERSION | Theme: $HAS_THEME"
67
68 # --- Copy source files ---
69 SRC_DIR="$BACKUP/$CLIENT_DIR/src"
70 if [ "$CLIENT_DIR" = "src" ]; then
71 SRC_DIR="$BACKUP/src"
72 fi
73
74 if [ -d "$SRC_DIR" ]; then
75 cp -r "$SRC_DIR"/* "$ARTIFACT_DIR/src/"
76 echo "COPIED: $CLIENT_DIR/src/* → $ARTIFACT_DIR/src/"
77 else
78 echo "WARNING: $SRC_DIR not found — agent must copy source files manually"
79 fi
80
81 # --- Copy public assets ---
82 PUBLIC_DIR="$BACKUP/$CLIENT_DIR/public"
83 if [ "$CLIENT_DIR" = "src" ]; then
84 PUBLIC_DIR="$BACKUP/public"
85 fi
86
87 if [ -d "$PUBLIC_DIR" ]; then
88 cp -r "$PUBLIC_DIR"/* "$ARTIFACT_DIR/public/" 2>/dev/null || true
89 echo "COPIED: public/* → $ARTIFACT_DIR/public/"
90 fi
91
92 # --- Copy index.html ---
93 INDEX_HTML="$BACKUP/$CLIENT_DIR/index.html"
94 if [ "$CLIENT_DIR" = "src" ]; then
95 INDEX_HTML="$BACKUP/index.html"
96 fi
97
98 if [ -f "$INDEX_HTML" ]; then
99 cp "$INDEX_HTML" "$ARTIFACT_DIR/index.html"
100 echo "COPIED: index.html → $ARTIFACT_DIR/index.html"
101
102 # Warn about meta image paths that reference source assets instead of public/
103 for meta_attr in "og:image" "twitter:image"; do
104 META_PATH=$(grep -oE "property=\"$meta_attr\"[^>]*content=\"[^\"]+\"" "$ARTIFACT_DIR/index.html" 2>/dev/null | grep -oE 'content="[^"]+"' | sed 's/content="//;s/"$//' || true)
105 if [ -n "$META_PATH" ] && echo "$META_PATH" | grep -qv "^http"; then
106 # Check if the referenced file exists in public/
107 if [ ! -f "$ARTIFACT_DIR/public${META_PATH}" ] && [ ! -f "$ARTIFACT_DIR/public/$META_PATH" ]; then
108 echo "WARNING: $meta_attr references $META_PATH which is not in public/ — social previews may break"
109 fi
110 fi
111 done
112 else
113 echo "WARNING: index.html not found at $INDEX_HTML — check if fonts/meta tags need manual copy"
114 fi
115
116 # --- Copy local Vite plugin files ---
117 # If the old vite.config imports local files (./vite-plugin-foo), copy them
118 VITE_CFG=""
119 for candidate in "$BACKUP/vite.config.ts" "$BACKUP/client/vite.config.ts" "$BACKUP/vite.config.js" "$BACKUP/client/vite.config.js"; do
120 [ -f "$candidate" ] && VITE_CFG="$candidate" && break
121 done
122 if [ -n "$VITE_CFG" ]; then
123 LOCAL_IMPORTS=$(grep -oE "from ['\"]\\./[^'\"]+['\"]" "$VITE_CFG" 2>/dev/null | sed "s/from [\"']//;s/[\"']//" || true)
124 for imp in $LOCAL_IMPORTS; do
125 # Resolve relative to the vite config's directory
126 VITE_DIR=$(dirname "$VITE_CFG")
127 SRC_FILE="$VITE_DIR/$imp"
128 # Try with .ts extension if no extension
129 [ ! -f "$SRC_FILE" ] && SRC_FILE="${SRC_FILE}.ts"
130 [ ! -f "$SRC_FILE" ] && SRC_FILE="${VITE_DIR}/${imp}.js"
131 if [ -f "$SRC_FILE" ]; then
132 cp "$SRC_FILE" "$ARTIFACT_DIR/"
133 echo "COPIED: local Vite plugin $(basename "$SRC_FILE") → $ARTIFACT_DIR/"
134 fi
135 done
136 fi
137
138 # --- Tailwind v3 handling ---
139 VITE_EDITS_NEEDED=""
140 if [ "$TW_VERSION" = "v3" ]; then
141 echo ""
142 echo "--- Tailwind v3 setup ---"
143
144 # Copy tailwind config
145 TW_CONFIG_COPIED="false"
146 for candidate in \
147 "$BACKUP/tailwind.config.ts" \
148 "$BACKUP/tailwind.config.js" \
149 "$BACKUP/tailwind.config.cjs" \
150 "$BACKUP/$CLIENT_DIR/tailwind.config.ts" \
151 "$BACKUP/$CLIENT_DIR/tailwind.config.js"; do
152 if [ -f "$candidate" ]; then
153 cp "$candidate" "$ARTIFACT_DIR/"
154 echo "COPIED: $(basename "$candidate") → $ARTIFACT_DIR/"
155 TW_CONFIG_COPIED="true"
156 break
157 fi
158 done
159 if [ "$TW_CONFIG_COPIED" = "false" ]; then
160 echo "WARNING: No tailwind config found — agent may need to create one"
161 else
162 # Fix content paths — old configs reference ./client/src/**, ./app/**, etc.
163 # After migration, source lives at ./src/** relative to artifact root.
164 for tw_file in "$ARTIFACT_DIR"/tailwind.config.*; do
165 [ -f "$tw_file" ] || continue
166 sed -i 's|"\./client/src/|"./src/|g' "$tw_file" 2>/dev/null || true
167 sed -i "s|'./client/src/|'./src/|g" "$tw_file" 2>/dev/null || true
168 sed -i 's|"\./client/|"./|g' "$tw_file" 2>/dev/null || true
169 sed -i "s|'./client/|'./|g" "$tw_file" 2>/dev/null || true
170 sed -i 's|"\./app/|"./src/|g' "$tw_file" 2>/dev/null || true
171 sed -i "s|'./app/|'./src/|g" "$tw_file" 2>/dev/null || true
172 done
173 echo "FIXED: tailwind content paths updated for artifact structure"
174 fi
175
176 # Install v3 deps, remove v4 plugin
177 pnpm --filter "@workspace/$APP_NAME" add -D tailwindcss@3 postcss autoprefixer 2>&1 || true
178 pnpm --filter "@workspace/$APP_NAME" remove @tailwindcss/vite 2>&1 || true
179 echo "INSTALLED: tailwindcss@3 postcss autoprefixer"
180 echo "REMOVED: @tailwindcss/vite"
181
182 VITE_EDITS_NEEDED="$VITE_EDITS_NEEDED
183 - Remove @tailwindcss/vite import and plugin entry
184 - Add css.postcss.plugins with autoprefixer + tailwindcss"
185 fi
186
187 # --- Theme.json handling ---
188 if [ "$HAS_THEME" = "true" ]; then
189 echo ""
190 echo "--- Theme setup ---"
191
192 cp "$BACKUP/theme.json" "$ARTIFACT_DIR/theme.json"
193 echo "COPIED: theme.json → $ARTIFACT_DIR/theme.json"
194
195 pnpm --filter "@workspace/$APP_NAME" add -D @replit/vite-plugin-shadcn-theme-json 2>&1 || true
196 echo "INSTALLED: @replit/vite-plugin-shadcn-theme-json"
197
198 VITE_EDITS_NEEDED="$VITE_EDITS_NEEDED
199 - Add: import themePlugin from \"@replit/vite-plugin-shadcn-theme-json\"
200 - Add: themePlugin() to plugins array"
201 fi
202
203 # --- Copy postcss config only for Tailwind v3 ---
204 # For v4, the scaffold uses @tailwindcss/vite — a v3 postcss config would conflict.
205 if [ "$TW_VERSION" = "v3" ]; then
206 for candidate in "$BACKUP/postcss.config.js" "$BACKUP/postcss.config.cjs" "$BACKUP/postcss.config.mjs"; do
207 if [ -f "$candidate" ]; then
208 cp "$candidate" "$ARTIFACT_DIR/"
209 echo "COPIED: $(basename "$candidate") → $ARTIFACT_DIR/"
210 break
211 fi
212 done
213 else
214 # Flag if old postcss config exists but we're skipping it
215 for candidate in "$BACKUP/postcss.config.js" "$BACKUP/postcss.config.cjs" "$BACKUP/postcss.config.mjs"; do
216 if [ -f "$candidate" ]; then
217 echo "SKIPPED: $(basename "$candidate") — conflicts with @tailwindcss/vite (Tailwind v4)"
218 break
219 fi
220 done
221 fi
222
223 # --- Fix Vite fs.strict for @assets alias ---
224 # The scaffold sets fs.strict: true, which blocks access to attached_assets/ outside the artifact root.
225 # If attached_assets exist, the agent needs to flip fs.strict to false.
226 if [ -d "$BACKUP/attached_assets" ] || [ -d "attached_assets" ]; then
227 VITE_EDITS_NEEDED="$VITE_EDITS_NEEDED
228 - Set server.fs.strict to false (attached_assets lives outside artifact root)"
229 fi
230
231 # --- Auto-patch fonts from index.html into index.css ---
232 FONT_PATCHED="false"
233 if [ -f "$ARTIFACT_DIR/index.html" ] && [ -f "$ARTIFACT_DIR/src/index.css" ]; then
234 # Extract font family names from Google Fonts URL
235 FONT_FAMILIES=$(grep -oE 'family=[^&"]+' "$ARTIFACT_DIR/index.html" 2>/dev/null | sed 's/family=//g; s/+/ /g; s/:.*//g' | head -3 || true)
236 if [ -n "$FONT_FAMILIES" ]; then
237 PRIMARY_FONT=$(echo "$FONT_FAMILIES" | head -1)
238 if [ -n "$PRIMARY_FONT" ]; then
239 # Try to update --app-font-sans or --font-sans in index.css
240 if grep -q "\-\-app-font-sans:" "$ARTIFACT_DIR/src/index.css" 2>/dev/null; then
241 sed -i "s/--app-font-sans:.*$/--app-font-sans: '$PRIMARY_FONT', sans-serif;/" "$ARTIFACT_DIR/src/index.css" 2>/dev/null && FONT_PATCHED="true"
242 elif grep -q "\-\-font-sans:" "$ARTIFACT_DIR/src/index.css" 2>/dev/null; then
243 sed -i "s/--font-sans:.*$/--font-sans: '$PRIMARY_FONT', sans-serif;/" "$ARTIFACT_DIR/src/index.css" 2>/dev/null && FONT_PATCHED="true"
244 fi
245 if [ "$FONT_PATCHED" = "true" ]; then
246 echo "PATCHED: index.css — updated font-sans to '$PRIMARY_FONT'"
247 else
248 echo "FONT DETECTED: '$PRIMARY_FONT' in index.html — agent must update CSS variable manually"
249 fi
250 fi
251 fi
252 fi
253
254 # --- Detect wouter usage and BASE_URL requirement ---
255 NEEDS_WOUTER_BASE="false"
256 APP_TSX="$ARTIFACT_DIR/src/App.tsx"
257 if [ -f "$APP_TSX" ]; then
258 if grep -q "from ['\"]wouter['\"]" "$APP_TSX" 2>/dev/null; then
259 if ! grep -q "BASE_URL\|base=" "$APP_TSX" 2>/dev/null; then
260 NEEDS_WOUTER_BASE="true"
261 # Auto-patch the import to include Router
262 if ! grep -q "Router" "$APP_TSX" 2>/dev/null; then
263 sed -i "s/from ['\"]wouter['\"]/from 'wouter'/" "$APP_TSX" 2>/dev/null || true
264 # Add Router import if Switch or Route is imported
265 sed -i "s/import { \(.*\) } from 'wouter'/import { \1, Router as WouterRouter } from 'wouter'/" "$APP_TSX" 2>/dev/null || true
266 echo "PATCHED: App.tsx — added WouterRouter import"
267 fi
268 fi
269 fi
270 fi
271
272 # --- Detect custom queryClient ---
273 HAS_QC="false"
274 for qc in "$ARTIFACT_DIR/src/lib/queryClient.ts" "$ARTIFACT_DIR/src/lib/queryClient.tsx" "$ARTIFACT_DIR/src/queryClient.ts"; do
275 if [ -f "$qc" ]; then
276 HAS_QC="true"
277 break
278 fi
279 done
280
281 # --- Scan for missing dependencies ---
282 # Extract third-party imports from copied source, compare against package.json,
283 # and install anything missing in one batch.
284 ARTIFACT_PKG="$ARTIFACT_DIR/package.json"
285 if [ -f "$ARTIFACT_PKG" ]; then
286 # Node built-ins to skip
287 BUILTINS="assert buffer child_process cluster console crypto dgram dns domain events fs http http2 https inspector module net os path perf_hooks process punycode querystring readline repl stream string_decoder sys timers tls trace_events tty url util v8 vm worker_threads zlib"
288
289 # Extract package names from imports. For scoped packages (@scope/name),
290 # keep @scope/name. For unscoped (react, wouter), keep just the name.
291 # Skip: relative imports, @/ aliases, @shared/ workspace refs, @workspace/
292 MISSING_DEPS=$(find "$ARTIFACT_DIR/src" \( -name "*.ts" -o -name "*.tsx" \) -print0 2>/dev/null | \
293 xargs -0 grep -ohE "from ['\"][^'\"./@][^'\"]*['\"]|from ['\"]@[a-z][^'\"]+['\"]" 2>/dev/null | \
294 sed "s/from ['\"]//;s/['\"]$//" | \
295 sed -E 's|^(@[^/]+/[^/]+)/.*|\1|; s|^([^@][^/]*)/.*|\1|' | \
296 sort -u | \
297 while read -r pkg; do
298 [ -z "$pkg" ] && continue
299 # Skip workspace/alias packages
300 case "$pkg" in @workspace/*|@shared/*|@db/*|@assets/*) continue ;; esac
301 # Skip node built-ins
302 echo " $BUILTINS " | grep -q " $pkg " && continue
303 # Skip if already in package.json
304 grep -q "\"$pkg\"" "$ARTIFACT_PKG" 2>/dev/null && continue
305 echo "$pkg"
306 done || true)
307
308 if [ -n "$MISSING_DEPS" ]; then
309 DEP_LIST=$(echo "$MISSING_DEPS" | tr '\n' ' ')
310 echo ""
311 echo "--- Installing missing dependencies ---"
312 echo "DETECTED: $DEP_LIST"
313 # Intentional word splitting for package list
314 # shellcheck disable=SC2086
315 pnpm --filter "@workspace/$APP_NAME" add $DEP_LIST 2>&1 || true
316 echo "INSTALLED: $DEP_LIST"
317 fi
318 fi
319
320 echo ""
321 echo "==== FRONTEND COPY COMPLETE ===="
322
323 if [ -n "$VITE_EDITS_NEEDED" ]; then
324 echo ""
325 echo "AGENT TODO — edit $ARTIFACT_DIR/vite.config.ts:$VITE_EDITS_NEEDED"
326 fi
327 echo ""
328 echo "AGENT TODO:"
329 echo " 1. Fix frontend imports (@shared/ → generated hooks from @workspace/api-client-react)"
330 if [ "$NEEDS_WOUTER_BASE" = "true" ]; then
331 echo " 2. REQUIRED: Wrap wouter Router with base path — add <WouterRouter base={import.meta.env.BASE_URL.replace(/\\/$/, '')}> around your routes in App.tsx. Navigation will silently break without this."
332 fi
333 if [ "$HAS_QC" = "true" ]; then
334 echo " 3. Custom queryClient.ts was copied — verify App.tsx imports from it instead of creating a bare new QueryClient()"
335 fi