Skip to content

Commit c72fccf

Browse files
yomybabyclaude
andcommitted
fix(FR-2607): align Electron publicPath patch with Vite output
Resolves the gap between PR #6871 (Vite spike for `es6://` publicPath via `renderBuiltUrl`) and PR #6765 / FR-2612 (single React build + post-patch policy). The post-patch script was still searching for CRA-era `/static/...` paths after the Vite migration emitted `/assets/...`, so `make dep` produced an Electron staging dir that referenced root-relative `/assets/...` that Electron could not serve via the `es6://` scheme handler. - `bash scripts/verify.sh` → Relay / Lint / Format / TypeScript ALL PASS - `make clean && make dep` → "Patched 2 file(s). Verification passed: index.html contains es6://assets/" - `make dep` run twice → idempotent skip ("Electron app already prepared, skipping") - `pnpm run electron:d` → renderer spawns with `es6://` scheme registered, login UI renders, all stylesheets load (rule counts > 0), body bg = rgb(247, 247, 246), `did-fail-load` / `webRequest err` / 404s = 0 - `make mac_arm64` → `compile_localproxy` + `electron-packager` (.app) produced cleanly. (DMG step fails on a local `NODE_MODULE_VERSION` mismatch in a globally-installed `electron-installer-dmg`; unrelated to this fix and does not affect CI which uses `npx`.) - Replace CRA-era patch patterns (`/static/js`, `/static/css`, `asset-manifest.json`, webpack runtime `.p='/'` rewrites) with Vite-aware patterns. - Patch `index.html`: rewrite `="/assets/` → `="es6://assets/` on `src`/`href` attributes only. - Patch CSS files under `assets/`: `url(/assets/...)` → `url(es6://assets/...)` in all quote styles. - Drop dead Webpack-specific code paths (`asset-manifest.json`, `static/{js,css}`, webpack runtime `publicPath` assignment) that no longer exist in Vite output. - Update verification marker from `es6://static/js/main` to `es6://assets/`. - Leave `<base href="/">` untouched: the renderer loads `index.html` via `file://`, and rewriting `<base>` to `es6://` would break `window.location`/origin alignment (history API, same-origin checks). Webpack-era patch script also did not touch `<base>`. - Verification failure now exits non-zero so the Makefile recipe aborts instead of silently continuing. - Update `dep_electron` idempotency marker from `es6://static/js/main` to `es6://assets/` to match the new patch script. - Replace `;` chaining with `&&` between the `dep_electron` commands so a failed patch script aborts the recipe. - Update inline comment that referenced the old marker. - Remove the `BUILD_TARGET=electron` + `experimental.renderBuiltUrl` branch (PR #6871 spike). FR-2612 mandates a single web build with post-patch; the `renderBuiltUrl` path would require a second `vite build` per release and was never wired into the Makefile, making it dead code with regression risk if anyone toggled `BUILD_TARGET=electron`. - Drop the now-unused `command` parameter from `defineConfig`. - Remove three unused CRA-era devDependencies: `@svgr/webpack` (Vite uses `vite-plugin-svgr`), `react-scripts` (CRA core, retired in FR-2611), `react-dev-utils` (no imports anywhere). All three were verified to have zero references in source. Sits on top of #7113 (FR-2606 residual build-time warnings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 467c14c commit c72fccf

4 files changed

Lines changed: 56 additions & 105 deletions

File tree

Makefile

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,27 @@ dep_web:
9797
# Uses publicPath patching instead of a full second React build (~4-8 min savings).
9898
#
9999
# Idempotent: skips when `build/electron-app/app/index.html` already carries
100-
# the patched `es6://static/js/main` marker. This mirrors the original
100+
# the patched `es6://assets/` marker. This mirrors the original
101101
# Makefile's skip semantics so downstream targets that re-declare `dep` as a
102102
# prerequisite (e.g. `mac_x64`, `win_x64`) do not repeatedly re-copy the web
103103
# bundle. Set `FORCE_DEP_ELECTRON=1` to force a re-sync.
104104
dep_electron: dep_web
105-
@if [ -f "./build/electron-app/app/index.html" ] && grep -q 'es6://static/js/main' ./build/electron-app/app/index.html && [ "$(FORCE_DEP_ELECTRON)" != "1" ]; then \
105+
@if [ -f "./build/electron-app/app/index.html" ] && grep -q 'es6://assets/' ./build/electron-app/app/index.html && [ "$(FORCE_DEP_ELECTRON)" != "1" ]; then \
106106
printf "$(YELLOW)Electron app already prepared, skipping$(NC)\n"; \
107107
else \
108108
if [ ! -d "./build/electron-app" ]; then \
109109
mkdir -p build/electron-app; \
110110
cp -r electron-app/* build/electron-app/; \
111111
cp electron-app/.npmrc build/electron-app/; \
112112
pnpm i --prefix ./build/electron-app --ignore-workspace; \
113-
fi; \
114-
rm -rf build/electron-app/app build/electron-app/resources build/electron-app/manifest; \
115-
cp -Rp build/web build/electron-app/app; \
116-
cp -Rp build/web/resources build/electron-app; \
117-
cp -Rp build/web/manifest build/electron-app; \
118-
node scripts/patch-electron-publicpath.js build/electron-app/app; \
119-
mkdir -p ./build/electron-app/app/wsproxy; \
120-
cp ./src/wsproxy/dist/wsproxy.js ./build/electron-app/app/wsproxy/wsproxy.js; \
113+
fi && \
114+
rm -rf build/electron-app/app build/electron-app/resources build/electron-app/manifest && \
115+
cp -Rp build/web build/electron-app/app && \
116+
cp -Rp build/web/resources build/electron-app && \
117+
cp -Rp build/web/manifest build/electron-app && \
118+
node scripts/patch-electron-publicpath.js build/electron-app/app && \
119+
mkdir -p ./build/electron-app/app/wsproxy && \
120+
cp ./src/wsproxy/dist/wsproxy.js ./build/electron-app/app/wsproxy/wsproxy.js && \
121121
cp ./preload.js ./build/electron-app/preload.js; \
122122
fi
123123
dep: dep_electron

react/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"@microsoft/fetch-event-source": "^2.0.1",
2424
"@monaco-editor/react": "^4.7.0",
2525
"@react-hook/resize-observer": "^2.0.2",
26-
"@svgr/webpack": "^8.1.0",
2726
"@tanstack/react-query": "catalog:",
2827
"@testing-library/react": "catalog:",
2928
"@uiw/codemirror-extensions-langs": "^4.25.9",
@@ -152,9 +151,7 @@
152151
"jsdom": "^29.0.2",
153152
"nodemon": "^3.1.14",
154153
"prop-types": "^15.8.1",
155-
"react-dev-utils": "^12.0.1",
156154
"react-grab": "^0.1.32",
157-
"react-scripts": "5.0.1",
158155
"react-test-renderer": "^19.2.5",
159156
"relay-compiler": "catalog:",
160157
"relay-test-utils": "catalog:",

react/vite.config.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ function monacoStaticPlugin(): Plugin {
278278
};
279279
}
280280

281-
export default defineConfig(({ command, mode }) => {
281+
export default defineConfig(({ mode }) => {
282282
const env = loadEnv(mode, projectRoot, '');
283283
Object.assign(process.env, env);
284284

@@ -297,31 +297,11 @@ export default defineConfig(({ command, mode }) => {
297297
.filter(Boolean)
298298
: undefined;
299299

300-
// Electron target uses a custom `es6://` URL scheme; the main process
301-
// registers a protocol handler via `protocol.handle('es6', ...)` in
302-
// `electron-app/main.js` that resolves `es6://foo` to
303-
// `<electron-app>/app/foo` on disk. This matches the craco-era
304-
// `webpackConfig.output.publicPath = 'es6://'` override in
305-
// `react/craco.config.cjs`.
306-
//
307-
// We cannot set `base: 'es6://'` directly: Vite's external-URL regex is
308-
// `/^([a-z]+:)?\/\//`, and the `6` in `es6` breaks the `[a-z]+` group,
309-
// so Vite treats `es6://` as a malformed absolute path and strips the
310-
// scheme. The official escape hatch is `experimental.renderBuiltUrl`,
311-
// which is called per built asset and whose return value replaces the
312-
// default `base + filename` concatenation — no scheme validation.
313-
const isElectronBuild =
314-
command === 'build' && process.env.BUILD_TARGET === 'electron';
315-
300+
// The Electron target's `es6://` publicPath is applied by
301+
// `scripts/patch-electron-publicpath.js` after the single web build
302+
// (per FR-2612 policy: one build, post-patch — never a second `vite build`).
316303
return {
317304
base: '/',
318-
experimental: isElectronBuild
319-
? {
320-
renderBuiltUrl(filename) {
321-
return `es6://${filename}`;
322-
},
323-
}
324-
: undefined,
325305
// Keep root at `react/` so pnpm's react/react-dom installed in
326306
// react/node_modules resolve correctly. Static assets from projectRoot
327307
// are handled by projectRootStaticPlugin above.
Lines changed: 42 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Patch the web build output to use Electron's 'es6://' publicPath.
4+
* Patch the Vite web build output to use Electron's 'es6://' publicPath.
55
*
6-
* When building for Electron, webpack's output.publicPath must be 'es6://'
7-
* instead of the default '/'. Previously this required a full second React
8-
* build (~4-8 min). This script achieves the same result by patching the
9-
* already-built files, saving significant CI time.
6+
* When building for Electron, asset URLs must use the custom 'es6://' scheme
7+
* (handled by electron-app/main.js) instead of the default '/'. Doing this
8+
* with a second `vite build` (BUILD_TARGET=electron + experimental.renderBuiltUrl)
9+
* doubles release CI time, which FR-2612 explicitly eliminated. This script
10+
* keeps the single-build model by patching the already-built files.
1011
*
1112
* Usage: node scripts/patch-electron-publicpath.js <build-dir>
1213
*
13-
* The script patches:
14-
* 1. index.html — static asset references (/static/... → es6://static/...)
15-
* 2. asset-manifest.json — all asset paths
16-
* 3. JS bundles — webpack runtime's publicPath assignment (e.g. n.p="/")
17-
* 4. CSS files — url() references to /static/ assets
18-
* 5. Service worker (sw.js) — precache manifest URLs
14+
* Vite's output layout (post-FR-2606 migration):
15+
* - index.html with `<base href="/">` and `<script src="/assets/...">`
16+
* - assets/*.js, assets/*.css, assets/<fonts/images>
17+
* - sw.js + workbox-*.js (precache uses relative URLs, no patch needed)
18+
* - JS chunks reference siblings via ESM imports already resolved by the HTML
19+
* entry, so chunk file contents themselves carry no '/assets/' literals.
20+
*
21+
* Patched targets:
22+
* 1. index.html — <base href> + script/link tags pointing to /assets/
23+
* 2. CSS files under assets/ — url(/assets/...) references (fonts, images)
1924
*/
2025

2126
const fs = require('fs');
@@ -33,14 +38,10 @@ if (!fs.existsSync(buildDir)) {
3338
process.exit(1);
3439
}
3540

36-
const WEB_PUBLIC_PATH = '/';
3741
const ELECTRON_PUBLIC_PATH = 'es6://';
3842

3943
let patchedCount = 0;
4044

41-
/**
42-
* Replace all occurrences of web publicPath with electron publicPath in a file.
43-
*/
4445
function patchFile(filePath, replacements) {
4546
if (!fs.existsSync(filePath)) return false;
4647

@@ -60,76 +61,49 @@ function patchFile(filePath, replacements) {
6061
return false;
6162
}
6263

63-
console.log(`Patching publicPath: "${WEB_PUBLIC_PATH}" → "${ELECTRON_PUBLIC_PATH}"`);
64+
console.log(`Patching publicPath: "/" → "${ELECTRON_PUBLIC_PATH}"`);
6465
console.log(`Build directory: ${buildDir}\n`);
6566

66-
// 1. Patch index.html
67+
// 1. Patch index.html — only root-absolute refs
68+
//
69+
// Note: <base href="/"> is intentionally NOT patched. The webpack-era patch
70+
// script left it untouched too. The renderer loads index.html via file://,
71+
// and rewriting <base> to es6:// causes window.location/origin mismatches
72+
// (history API, same-origin checks). Relative refs like "manifest/foo" and
73+
// "resources/bar.css" resolve against the file:// document URL and read
74+
// from the app dir, which is what Electron actually serves.
6775
patchFile(path.join(buildDir, 'index.html'), [
68-
// Script/link tags: src="/static/... → src="es6://static/...
69-
['="/static/', `="${ELECTRON_PUBLIC_PATH}static/`],
70-
// Href references
71-
['href="/static/', `href="${ELECTRON_PUBLIC_PATH}static/`],
72-
// Manifest and other root-relative references
73-
['="/manifest', `="${ELECTRON_PUBLIC_PATH}manifest`],
76+
// src="/assets/... or href="/assets/...
77+
['="/assets/', `="${ELECTRON_PUBLIC_PATH}assets/`],
7478
]);
7579

76-
// 2. Patch asset-manifest.json
77-
patchFile(path.join(buildDir, 'asset-manifest.json'), [
78-
[`"/static/`, `"${ELECTRON_PUBLIC_PATH}static/`],
79-
]);
80-
81-
// 3. Patch JS bundles (webpack runtime publicPath + chunk references)
82-
const jsDir = path.join(buildDir, 'static', 'js');
83-
if (fs.existsSync(jsDir)) {
84-
const jsFiles = fs.readdirSync(jsDir).filter((f) => f.endsWith('.js'));
85-
for (const file of jsFiles) {
86-
patchFile(path.join(jsDir, file), [
87-
// Webpack runtime: various minified forms of __webpack_require__.p = "/"
88-
// Common patterns from webpack 5 + terser:
89-
['.p="/"', `.p="${ELECTRON_PUBLIC_PATH}"`],
90-
[".p='/'", `.p='${ELECTRON_PUBLIC_PATH}'`],
91-
// Static references to /static/ in chunk loading code
92-
['"/static/', `"${ELECTRON_PUBLIC_PATH}static/`],
93-
]);
94-
}
95-
}
96-
97-
// 4. Patch CSS files (url() references)
98-
const cssDir = path.join(buildDir, 'static', 'css');
99-
if (fs.existsSync(cssDir)) {
100-
const cssFiles = fs.readdirSync(cssDir).filter((f) => f.endsWith('.css'));
80+
// 2. Patch CSS files under assets/ — url(/assets/...) references for fonts/images
81+
const assetsDir = path.join(buildDir, 'assets');
82+
if (fs.existsSync(assetsDir)) {
83+
const cssFiles = fs.readdirSync(assetsDir).filter((f) => f.endsWith('.css'));
10184
for (const file of cssFiles) {
102-
patchFile(path.join(cssDir, file), [
103-
['url(/static/', `url(${ELECTRON_PUBLIC_PATH}static/`],
85+
patchFile(path.join(assetsDir, file), [
86+
['url(/assets/', `url(${ELECTRON_PUBLIC_PATH}assets/`],
87+
// Quoted forms (rare but allowed by CSS spec)
88+
['url("/assets/', `url("${ELECTRON_PUBLIC_PATH}assets/`],
89+
["url('/assets/", `url('${ELECTRON_PUBLIC_PATH}assets/`],
10490
]);
10591
}
10692
}
10793

108-
// 5. Patch service worker if present
109-
patchFile(path.join(buildDir, 'sw.js'), [
110-
['"/static/', `"${ELECTRON_PUBLIC_PATH}static/`],
111-
['url:"/static/', `url:"${ELECTRON_PUBLIC_PATH}static/`],
112-
]);
113-
114-
// 6. Patch workbox precache manifest if present
115-
const swFiles = fs.readdirSync(buildDir).filter((f) => /^workbox-.*\.js$/.test(f));
116-
for (const file of swFiles) {
117-
patchFile(path.join(buildDir, file), [
118-
['"/static/', `"${ELECTRON_PUBLIC_PATH}static/`],
119-
]);
120-
}
121-
12294
console.log(`\nDone. Patched ${patchedCount} file(s).`);
12395

12496
// Verify the patch worked by checking index.html
12597
const indexPath = path.join(buildDir, 'index.html');
12698
if (fs.existsSync(indexPath)) {
12799
const indexContent = fs.readFileSync(indexPath, 'utf-8');
128-
if (indexContent.includes('es6://static/js/main')) {
129-
console.log('✓ Verification passed: index.html contains es6://static/js/main');
100+
if (indexContent.includes(`${ELECTRON_PUBLIC_PATH}assets/`)) {
101+
console.log(`✓ Verification passed: index.html contains ${ELECTRON_PUBLIC_PATH}assets/`);
130102
} else {
131-
console.error('✗ Verification failed: index.html does not contain es6://static/js/main');
132-
console.error(' The Electron build may not work correctly.');
103+
console.error(
104+
`✗ Verification failed: index.html does not contain ${ELECTRON_PUBLIC_PATH}assets/`,
105+
);
106+
console.error(' The Electron build will not work correctly.');
133107
process.exit(1);
134108
}
135109
}

0 commit comments

Comments
 (0)