Skip to content

Mobile editors: fix not-loading + ES2022 target / i18next crash#156

Merged
moodyjmz merged 18 commits into
mainfrom
fix/mobile-es2022-i18next
Jul 1, 2026
Merged

Mobile editors: fix not-loading + ES2022 target / i18next crash#156
moodyjmz merged 18 commits into
mainfrom
fix/mobile-es2022-i18next

Conversation

@moodyjmz

@moodyjmz moodyjmz commented Jun 30, 2026

Copy link
Copy Markdown
Member

Two mobile fixes that landed together.

  1. Mobile editors don't load — reported in Regression: mobile editor doesn't load in latest-dev DocumentServer#258. The grunt→pipeline migration dropped the mobile deploy step, so the mobile folder shipped with only resources/ (404). Ported as deploy-mobile.js + verify-deploy assertions.
  2. ES2022 target / i18next crash — the mobile bundle built at esbuild target:'es2015' throws TypeError: we is not a function on launch. Fixed by centralising the browser target to ES2022.
Root cause + validation

Crash cause. The esbuild minifier swap (f0e183749c) set target:'es2015', which downlevels the whole bundle — including i18next 25's class-based internals. esbuild's ES2015 class-field lowering mis-compiles a Translator internal → we is not a function. Not webpack-vs-grunt and not the i18next dep bump (it ran fine under grunt's Terser build) — mobile was always a webpack build; only the minifier changed.

Fix. Collapse the four scattered, contradictory browser targets (babel >0.25%, esbuild es2015, postcss Chrome 49, decided floor) into a single source of truth build/browser-floor.mjs at ES2022 (Nextcloud floor: iOS 17 / Safari 17; Android 9 = updatable WebView). ES2022 keeps class fields native, so the breaking transform never runs. Also: restore drop_console (the esbuild swap silently dropped Terser's), make build mode explicit + echoed in the pipeline banner, decouple mobile watch to WATCH=1, and add a verify-browser-target gate that fails the build if a target literal (string/array/object) is hardcoded in a config instead of imported from browser-floor.mjs. (It's a static-source check — it asserts the import + absence of a literal, not that the imported value is the one wired in.)

Validation (production build in the eo dev container):

  • __publicField (esbuild class-field lowering helper) = 0; 79 native # private fields retained → the es2015 transform is gone.
  • i18next is ready = 0 in all four mobile bundles → drop_console restored.
  • All four mobile editors load and render in-browser (confirmed manually).
  • verify-browser-target passes, and fails on a planted hardcoded target (string, esnext, and array-literal shapes).

Review follow-ups (addressed): the two verify gates were hardened after review —
verify-browser-target now rejects array/object/esnext target literals (not just
ES-year strings), and verify-deploy asserts the actual dist/js/app.js entry rather
than only the directory. The build/README pipeline reference was also brought up to the
current 5-phase structure.

Fixes Euro-Office/DocumentServer#258
Follow-ups: #154 (CI mobile validation), Euro-Office/DocumentServer#259 (Makefile dev/prod targets)


Commits are AI-co-authored (Claude Sonnet 4.6) and adversarially reviewed/verified (Claude Opus); the fix was validated against a real production build + browser load. Human diff review still wanted.

moodyjmz and others added 7 commits June 30, 2026 20:11
Ports grunt's deploy-app-mobile task to the deploy-*.js family.
Copies mobile build output (index.html → index.html + index_loader.html,
dist/, css/*.css, locale/, resources/img/) from the source tree into
BUILD_ROOT for documenteditor, spreadsheeteditor, presentationeditor,
and visioeditor. Soft-skips any editor without mobile/index.html.

Copy-merge only — never wipes BUILD_ROOT. Runs before deploy-theme-images
so theme images overlay-win on resources/img/ (deploy-theme-images.js:69).

Fixes #258.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Insert deploy-mobile as Phase 2 (conditional on !SKIP_MOBILE) between
the Phase 1 mobile webpack builds and the Phase 3 deploy-resources +
deploy-theme-images pair. deploy-theme-images must run after deploy-mobile
because both write to <editor>/mobile/resources/img/ and theme images
must win the overlay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Add SKIP_MOBILE gate and per-editor mobile assertions:
- checkFile for index.html and index_loader.html
- checkDir for dist/, css/, locale/
- parse deployed index.html and assert each hashed CSS href exists
  (defense-in-depth against a copy glob too narrow to bring the
  content-hashed stylesheet across)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
The esbuild swap (f0e1837) set target:'es2015', downleveling the whole
bundle — including i18next 25's class-based Translator/Interpolator. esbuild's
ES2015 class-field lowering mis-compiled an internal → TypeError: we is not a
function on first t(). The same swap dropped Terser's drop_console, leaking
console logs into prod.

Root cause is the target, not webpack-vs-grunt or the dep bump — i18next 25 ran
fine under grunt's Terser build; mobile has always been a webpack build, only the
minifier changed.

Collapse the four scattered target declarations (babel >0.25%, esbuild es2015,
postcss Chrome 49, decided floor) into build/browser-floor.mjs — single source at
ES2022 (Nextcloud floor: iOS 17/Safari 17; Android 9 = updatable WebView). ES2022
keeps class fields native so the crash transform no longer runs; restore
drop_console for production.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Fail the build if any webpack/babel/postcss config hardcodes a browser target
instead of importing from build/browser-floor.mjs. Wired into the pipeline gates
phase. Stops the target drifting back into a buried minifier option as es2015 did.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Mobile webpack defaulted NODE_ENV to 'development', desktop to 'production' —
so an unset env produced a dev mobile bundle (esbuild off), which is how a
prod-only crash can look "fixed" in a dev rebuild.

Default NODE_ENV to production once in CHILD_ENV (honours an explicit
NODE_ENV=development), echo the resolved mode in the banner (green prod / red
DEV warning), and decouple mobile watch from NODE_ENV (WATCH=1 opt-in) so a
dev build is a one-shot. See Euro-Office/DocumentServer#259 for the companion
Makefile target cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz self-assigned this Jun 30, 2026
moodyjmz and others added 2 commits June 30, 2026 20:53
The pipeline grew from 2 to 5 phases (deploy-mobile = Phase 2, gates = Phase 5).
Bring the reference in line: 5-phase overview + matching YAML AI-context block,
deploy-mobile.js and verify-*.mjs in the script reference, browser-floor.mjs noted,
NODE_ENV/WATCH env vars, corrected deploy-app-mobile mapping, sprites.sh -> deploy-sprites.js,
mobile-builds-to-source-tree + single-browser-target invariants, and mark the done
"future improvements" (CI build-pipeline, Makefile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Two gates over-claimed their coverage:
- verify-browser-target.mjs: the target/targets regexes only caught ES-year
  string literals, so target:'esnext', "es6", array literals (['safari15']),
  and any array/object babel targets sailed through. Broaden to reject any
  string OR array/object literal after target:/targets: (allowed form is the
  bare ESBUILD_TARGET / BROWSERSLIST identifier). Header comment corrected to
  state it is a static-source check, not proof the imported value is wired in.
- verify-deploy.mjs: checkDir(dist) passed on a dist/ holding only a chunk or
  .map files, while a comment claimed the JS was covered. Assert the stable
  dist/js/app.js explicitly and fix the comment.

Verified: gate still passes clean, now trips on planted 'esnext' + array targets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz force-pushed the fix/mobile-es2022-i18next branch from ccc0e89 to cd345e3 Compare June 30, 2026 19:01
moodyjmz and others added 2 commits June 30, 2026 21:52
verify-browser-target only required a safari* entry in ESBUILD_TARGET (and
iOS/Safari in BROWSERSLIST), so it guarded only the iOS half of the floor.
Dropping the chrome entry would pass while silently un-guarding Android —
esbuild would then assume Chrome matches Safari's feature set. Add symmetric
Android/Chrome presence checks. Presence only (not version consistency): and_chr
is latest-only in caniuse, so pinning Android versions is meaningless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…anspiled

Record the scope so nobody "unifies" desktop onto this floor: desktop editors
ship the legacy source un-transpiled (Terser minify-only, mangle:false, no
babel/esbuild target, no autoprefixer) and have no explicit floor — transpiling
them would risk the dynamic property-access / `var Common = Common || {}`
patterns documented in build/README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
moodyjmz and others added 2 commits June 30, 2026 22:20
The "AI context — structured data" yaml block mirrored the prose above it
(phases, env vars, invariants, build outputs) in a second format inside the same
file — two copies that drift. It just demonstrated the cost: the 2→5 phase update
had to be made twice. Nothing consumes it (no tool parses the README), and modern
AI reads the prose fine. Keep the prose as the single source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
The README documents the current build system; the grunt→webpack migration
history is noise here (it lives in docs/webpack-migration.md and git). Remove the
"What this replaced" task-mapping table, the obsolete "grunt ran before webpack"
invariant (renumber the rest), and the "Replaces grunt X" lead-ins from the script
descriptions and intro.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz marked this pull request as ready for review June 30, 2026 20:37
@moodyjmz moodyjmz requested a review from a team as a code owner June 30, 2026 20:37
@moodyjmz moodyjmz requested review from Aiiaiiio, Alex-Arsys and j-base64 and removed request for a team June 30, 2026 20:37
moodyjmz and others added 2 commits July 1, 2026 01:10
verify-browser-target hardcoded the per-config regexes and per-engine presence
checks in its logic — target knowledge living outside the single source of truth.
Move the contracts into browser-floor.mjs as data (TARGET_CONTRACTS,
REQUIRED_ENGINES); the gate is now a generic runner. Adding a config consumer or a
required engine (e.g. Firefox) is a new row in the source, not a gate code change.

Also flips the wiring check from negative literal-detection ("no target literal",
which had to enumerate es-year/esnext/array shapes) to positive assertion ("must
wire `target: ESBUILD_TARGET`") — catches every hardcode form without the gate
knowing what a target looks like.

Verified: passes clean; trips on a planted hardcode (es2015 and esnext) and a
dropped engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…ing configs

Applies the review of dedd4f4:
- Move TARGET_CONTRACTS / REQUIRED_ENGINES out of browser-floor.mjs (pure values
  again) into a sibling browser-floor.manifest.mjs — matching the repo's existing
  values↔manifest↔verifier pattern (theme.config.mjs ↔ replacements.manifest.mjs ↔
  verify-replacements.mjs). A values file shouldn't encode config paths or how a
  plugin spells its options.
- verify-browser-target.mjs: clean-fail (not a raw ENOENT crash) when a contract's
  config path is missing or moved.

Verified: passes clean; missing config → clean fail message; planted hardcode and
dropped engine still fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz requested a review from MonaAghili June 30, 2026 23:39
@MonaAghili

Copy link
Copy Markdown

Hey @moodyjmz reviewed it with help of Claude and I share it's findings with you

Reviewed the branch diff directly against the grunt configs it replaces and ran the browserslist query live. Two core fixes are correct. One real bug. One new behaviour worth flagging.


Core fix 1: deploy-mobile.js — correct

Cross-checked every copy step against build/documenteditor.json (the grunt config this replaces):

What gets copied grunt deploy-mobile.js
index.htmlindex.html + index_loader.html copy.index-page — both map to same source copyFileSync twice from same source ✓
css/*.css (no maps) copy.assets src: '*.css' copyDirFiltered({ include: ['*.css'] })
dist/** copy.assets src: '**' copyDir(distSrc, dest/dist)
locale/* copy.localization src: '*' copyDir(localeSrc, dest/locale)
resources/img/** (per-editor) copy.images-app copyDir(imgSrc, dest/resources/img)
apps/common/mobile/resources/img/** copy.images-common[0] copyDir(commonMobileImg, ...)
apps/common/main/resources/img/about/** copy.images-common[1] copyDir(aboutSrc, .../about)

resources/css/ not copied — that directory doesn't exist in the source tree; grunt's mobile.css task was a no-op. Not a gap.

The .gz files in the v9.3.2 image (index.html.gz etc.) come from documentserver-static-gzip.sh in document-server-package, which gzips everything in web-apps/ during packaging — not the web-apps build's job. Not a gap here either.

Phase ordering is correct: deploy-mobile (Phase 2) runs strictly after webpack writes to the source tree (Phase 1) and strictly before deploy-theme-images (Phase 3), which overwrites the same resources/img/ paths and must win.


Core fix 2: ES2022 target / i18next crash — correct

target: 'es2015' made esbuild emit __publicField() helpers to downlevel class fields. i18next 25's Translator uses native class fields. __publicField corrupts the prototype chain → TypeError: we is not a function. Moving to ['safari17', 'chrome111'] (ES2022) keeps class fields native. Right fix, right signal to check (__publicField count = 0).

dist/js/app.js stable-name assertion in verify-deploy is correct — webpack config confirms filename: 'dist/js/[name].js' with no hash, entry name app.


Bug: and_chr >= 111 doesn't work as a version floor

build/browser-floor.mjs:

export const BROWSERSLIST = ['iOS >= 17', 'Safari >= 17', 'and_chr >= 111', 'not dead'];

Ran this live against the installed browserslist:

and_chr >= 111  →  ['and_chr 132']   // single current version only
chrome >= 111   →  23 entries        // actual floor

browserslist does not track historical and_chr versions. and_chr >= 111 resolves to whatever the current and_chr is — not "Chrome 111 and above as a floor." The correct query for Android Chrome version floors is chrome >= 111 (browserslist resolves desktop Chrome historically, and it's the right V8 proxy for Android Chrome).

The verify-browser-target gate only checks that and_chr appears in the array — it doesn't validate that the version constraint resolves. So the gate passes and this is invisible.

Functionally harmless today (current Chrome >> 111, both support ES2022 natively, so babel/postcss output is identical). But the intent isn't landing.

Fix: 'and_chr >= 111''chrome >= 111'


New behaviour: drop: ['console', 'debugger']

-new EsbuildPlugin({ target: 'es2015', css: true })
+new EsbuildPlugin({ target: ESBUILD_TARGET, css: true, drop: env === 'production' ? ['console', 'debugger'] : [] })

This was not in the webpack config on main (verified). esbuild's drop: ['console'] removes the entire call expression including arguments — console.log(fn()) silently drops fn() in production. Intentional for a production bundle, but it's a new behaviour in this pipeline, not a restore.


Verdict: approve. Fix and_chr >= 111chrome >= 111 inline or as immediate follow-up.

browserslist tracks `and_chr` as latest-only, so `and_chr >= 111` resolves to a
single current version (`and_chr 132`), not a Chrome-111 floor. `chrome >= 111`
resolves historically (23 entries → 111) and is the standard proxy for the Android
Chrome / updatable-WebView floor. Harmless today — Safari 17 is the binding ES2022
constraint — but the Android intent wasn't encoded, and the gate's presence-only
check couldn't catch it. Consistent with bb1e85c's own note that pinning
`and_chr` versions is meaningless.

Reported-by: MonaAghili (PR #156 review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz

moodyjmz commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Thanks @MonaAghili — the and_chr >= 111 catch was right and is fixed in 700138d (chrome >= 111, which resolves as a real historical floor; and_chr is latest-only). Everything else corroborates too.

One point I'd push back on: the drop: ['console', 'debugger'] "new behaviour, not a restore" framing measures against the wrong baseline. main is already post-regression — the esbuild swap is what removed the console-dropping. The config immediately before that swap had it under Terser, so re-adding it restores prior pipeline behaviour rather than introducing new behaviour.

The side-effect caveat (console.log(fn()) drops fn()) is real but not new — Terser's drop_console did the same thing, so this reintroduces known behaviour, not a novel risk. Production-only, so no dev impact.

Evidence

f0e183749c^:vendor/framework7-react/build/webpack.config.js:88 (config just before the esbuild swap):

new TerserPlugin({ terserOptions: { compress: { drop_console: env === 'production' } } })

The swap replaced Terser with EsbuildPlugin({ target: 'es2015', css: true }) (now on main, no drop). This PR re-adds drop: ['console','debugger'].

Comment prepared with Claude (Opus), verified against the branch.

…atest-only"

The Check-3 comment rationalised the presence-only check via "and_chr is
latest-only" — which is now stale (the floor uses `chrome >= 111` since
700138d) and was the reasoning that masked the and_chr bug in the first place.
State the gate's actual limitation instead: it checks engine presence, not the
version values in the queries.

Reported-by: MonaAghili (PR #156 review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Mirror the code fix (700138d) in the migration doc — the BROWSERSLIST snippet
still quoted the old `and_chr >= 111`, which resolves to a single current version
rather than a Chrome-111 floor.

Reported-by: MonaAghili (PR #156 review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@MonaAghili

Copy link
Copy Markdown

LGTM!

@moodyjmz moodyjmz merged commit f649e56 into main Jul 1, 2026
3 checks passed
moodyjmz added a commit that referenced this pull request Jul 1, 2026
Phase 1 mobile installs/builds, Phase 2 deploy-mobile, and the
verify-deploy mobile assertions all ran unconditionally already
in CI once #156 landed — this just deletes the dead SKIP_MOBILE
branches so there's no escape hatch left to flip back on.

Refs #157

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Ali-Razmjoo pushed a commit to fork-the-planet/euro-office-web-apps that referenced this pull request Jul 2, 2026
CI never built mobile, so #258 (mobile shipped resources/-only) and
the es2015 i18next crash both shipped with green CI. This arms the
mobile deploy + verify-deploy gate that Euro-Office#156 added.

Refs Euro-Office#157

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression: mobile editor doesn't load in latest-dev

2 participants