|
| 1 | +"""Capture-only TanStack build adaptation; call only on a disposable copy.""" |
| 2 | +import json |
| 3 | +import re |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +CAPTURE_NOTE = '// html2wp capture copy only: retain native SSR; omit deployment bundling.' |
| 7 | +def has_cloudflare_nitro(text): |
| 8 | + # Treat strings/templates as single tokens; ignore commented examples. |
| 9 | + tokens = re.findall(r'''//[^\n]*|/\*[\s\S]*?\*/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|[\w$]+|[^\s]''', text) |
| 10 | + tokens = [t for t in tokens if not t.startswith(('//', '/*'))] |
| 11 | + return any(tokens[i:i+5] == ['nitro', ':', '{', 'preset', ':'] |
| 12 | + and tokens[i+5] in ('"cloudflare-module"', "'cloudflare-module'") |
| 13 | + for i in range(max(0, len(tokens)-5))) |
| 14 | + |
| 15 | + |
| 16 | +def lovable_version(work): |
| 17 | + try: |
| 18 | + package = json.loads((work / "package.json").read_text()) |
| 19 | + deps = {**(package.get("dependencies") or {}), **(package.get("devDependencies") or {})} |
| 20 | + return deps.get("@lovable.dev/vite-tanstack-config") |
| 21 | + except (OSError, ValueError, TypeError): |
| 22 | + return None |
| 23 | + |
| 24 | + |
| 25 | +class UnsupportedCaptureConfig(ValueError): |
| 26 | + pass |
| 27 | + |
| 28 | + |
| 29 | +def already_adapted(text): |
| 30 | + return bool(re.match( |
| 31 | + r'\Aimport \{ defineConfig as (__h2wpCaptureDefineConfig_*) \} from "@lovable\.dev/vite-tanstack-config";\n' |
| 32 | + + re.escape(CAPTURE_NOTE) + r'\nconst ([A-Za-z_$][\w$]*) = \(options = \{\}\) => \1\(\{ \.\.\.options, nitro: false \}\);\n' |
| 33 | + r'\s*export\s+default\s+\2\s*\(\s*\{', text)) |
| 34 | + |
| 35 | + |
| 36 | +def lovable_capture_config(text, work): |
| 37 | + """Use Lovable v2's supported no-deploy mode, preserving its SSR options. |
| 38 | +
|
| 39 | + Only the literal, single named-import/object-call form is rewritten. |
| 40 | + Unknown wrappers and function configs retain their existing behavior. |
| 41 | + This is not a deployment config or a rename of the Worker entrypoint. |
| 42 | + """ |
| 43 | + version = lovable_version(work) |
| 44 | + if version != '2.5.3': |
| 45 | + return text, None |
| 46 | + # Only the explicit deployment preset known to replace the SSR entry. |
| 47 | + if not has_cloudflare_nitro(text): |
| 48 | + return text, None |
| 49 | + match = re.match( |
| 50 | + r"\A\s*import\s*\{\s*defineConfig(?:\s+as\s+([A-Za-z_$][\w$]*))?\s*\}" |
| 51 | + r"\s*from\s*(['\"])@lovable\.dev/vite-tanstack-config\2\s*;?", text) |
| 52 | + if not match: |
| 53 | + return text, None |
| 54 | + local = match.group(1) or "defineConfig" |
| 55 | + if not re.match(r"\s*export\s+default\s+" + re.escape(local) + r"\s*\(\s*\{", text[match.end():]): |
| 56 | + return text, None |
| 57 | + alias = "__h2wpCaptureDefineConfig" |
| 58 | + while alias in text: |
| 59 | + alias += "_" |
| 60 | + replacement = ( |
| 61 | + f'import {{ defineConfig as {alias} }} from "@lovable.dev/vite-tanstack-config";\n' |
| 62 | + f'{CAPTURE_NOTE}\n' |
| 63 | + f'const {local} = (options = {{}}) => {alias}({{ ...options, nitro: false }});\n' |
| 64 | + ) |
| 65 | + return replacement + text[match.end():], "Lovable v2 capture build: Nitro deployment adapter disabled" |
| 66 | + |
| 67 | + |
| 68 | +TANSTACK_PRERENDER = "prerender: { enabled: true, crawlLinks: true }" |
| 69 | + |
| 70 | + |
| 71 | +def enable_tanstack_prerender(work): |
| 72 | + """Turn on TanStack Start's static prerender in the build copy's Vite |
| 73 | + config. Returns what was done, or None when no config could be patched |
| 74 | + (the build then fails loudly on "no index.html", as before).""" |
| 75 | + for name in ("vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"): |
| 76 | + cfg = Path(work) / name |
| 77 | + if cfg.is_file() and not cfg.is_symlink(): |
| 78 | + break |
| 79 | + else: |
| 80 | + return None |
| 81 | + text = cfg.read_text() |
| 82 | + text, capture_note = lovable_capture_config(text, Path(work)) |
| 83 | + if lovable_version(Path(work)) and has_cloudflare_nitro(text) and not capture_note and not already_adapted(text): |
| 84 | + raise UnsupportedCaptureConfig( |
| 85 | + 'Nitro cloudflare-module needs a compatible capture adapter; ' |
| 86 | + 'supported: @lovable.dev/vite-tanstack-config 2.5.3 with a direct ' |
| 87 | + 'named defineConfig import and literal export options. ' |
| 88 | + 'Configuration was left unchanged; do not force prerender on.') |
| 89 | + if re.search(r"prerender\s*:\s*\{[^}]*enabled\s*:\s*false", text): |
| 90 | + new = re.sub(r"(prerender\s*:\s*\{[^}]*enabled\s*:\s*)false", r"\1true", text, count=1) |
| 91 | + how = "prerender.enabled flipped to true" |
| 92 | + elif re.search(r"\bprerender\s*:", text): |
| 93 | + new = text |
| 94 | + how = "prerender already configured — left as authored" |
| 95 | + elif re.search(r"tanstackStart\s*:\s*\{", text): |
| 96 | + new = re.sub(r"(tanstackStart\s*:\s*\{)", r"\1 " + TANSTACK_PRERENDER + ",", text, count=1) |
| 97 | + how = "prerender added to tanstackStart: {…}" |
| 98 | + elif re.search(r"tanstackStart\(\s*\{", text): |
| 99 | + new = re.sub(r"(tanstackStart\(\s*\{)", r"\1 " + TANSTACK_PRERENDER + ",", text, count=1) |
| 100 | + how = "prerender added to tanstackStart({…})" |
| 101 | + elif re.search(r"tanstackStart\(\s*\)", text): |
| 102 | + new = re.sub(r"tanstackStart\(\s*\)", "tanstackStart({ " + TANSTACK_PRERENDER + " })", text, count=1) |
| 103 | + how = "prerender added to tanstackStart()" |
| 104 | + elif "@lovable.dev/vite-tanstack-config" in text and re.search(r"defineConfig\(\s*\{", text): |
| 105 | + new = re.sub(r"(defineConfig\(\s*\{)", r"\1 tanstackStart: { " + TANSTACK_PRERENDER + " },", text, count=1) |
| 106 | + how = "tanstackStart.prerender added to the Lovable config" |
| 107 | + elif "@lovable.dev/vite-tanstack-config" in text and re.search(r"defineConfig\(\s*\)", text): |
| 108 | + new = re.sub(r"defineConfig\(\s*\)", "defineConfig({ tanstackStart: { " + TANSTACK_PRERENDER + " } })", text, count=1) |
| 109 | + how = "tanstackStart.prerender added to the Lovable config" |
| 110 | + else: |
| 111 | + return None |
| 112 | + cfg.write_text(new) |
| 113 | + return f"{cfg.name}: {how}" + (f"; {capture_note}" if capture_note else "") |
0 commit comments