Skip to content

build: replace Grunt+r.js with webpack 5 — Phase E complete#109

Merged
moodyjmz merged 93 commits into
mainfrom
build/webpack-migration
Jun 29, 2026
Merged

build: replace Grunt+r.js with webpack 5 — Phase E complete#109
moodyjmz merged 93 commits into
mainfrom
build/webpack-migration

Conversation

@moodyjmz

@moodyjmz moodyjmz commented Jun 15, 2026

Copy link
Copy Markdown
Member

TL;DR

Grunt is gone. All build tasks now run through webpack 5 + Node.js scripts.

CI build time: ~7 minutes → ~2 minutes (cold runner, no cache).
Local build time (eo container, SKIP_MOBILE=1): ~101s.
Mobile build time per editor: ~146–179s → ~101–132s (EsbuildPlugin, see below).
All 6 editors (documenteditor, spreadsheeteditor, presentationeditor, visioeditor, pdfeditor, forms) runtime-validated. CI green.

Developed with Claude Code — AI-assisted analysis, debugging, and implementation.


Testing

Pull Euro-Office/DocumentServer#187

pull this PR, run as normal make local, then in docker (once) make sdkjs and make web-apps-dev, see if errors come up. Test Euro Office in browser. Look for console errors or anything else.

Build system improvements (2026-06-17)

Three follow-up commits on top of the Phase E work, addressing mobile build performance and pipeline robustness.

⚡ EsbuildPlugin replaces TerserPlugin + CssMinimizerPlugin (closes #107)

Why: Mobile webpack builds were taking 146–179s per editor with TerserPlugin doing JS minification and CssMinimizerPlugin doing CSS minification in two separate passes. esbuild's minifier handles both in a single pass and is significantly faster.

What changed: vendor/framework7-react/build/webpack.config.js — the two-entry optimization.minimizer array and the duplicate CssMinimizerPlugin in the production plugins array are replaced with a single new EsbuildPlugin({ target: 'es2015', css: true }). terser-webpack-plugin and css-minimizer-webpack-plugin removed from package.json; esbuild-loader added.

Validation — two full pipeline runs in eo container:

Task Before After run 1 After run 2
mobile:word 146–179s 130.4s 132.9s
mobile:cell 146–179s 127.2s 130.8s
mobile:slide 146–179s 122.5s 126.1s
mobile:visio 146–179s 101.4s 116.2s
Wall clock 176.9s 164.0s

All 21 pipeline tasks green both runs. The remaining mobile build time (~100–130s) is babel-loader + bundling — the minification step is no longer the bottleneck.

Note: Further improvement is possible by replacing babel-loader with esbuild-loader for JS transforms (~30–50s additional saving). Deferred — the mobile code uses MobX @inject legacy decorators which require experimentalDecorators: true and warrant a separate tested commit.

✨ Lazy task spawn + word-wrapped phase task list

Why: task() previously spawned child processes eagerly. Phase 1 builds phase1Tasks (deploy ×5 + webpack ×6) eagerly, then awaits mobile:install (~20s), then calls phase(). This meant the phase banner and task list were printed after the fast deploy tasks had already completed and printed their ✓ lines — output order was banner-after-results, which was confusing.

What changed: task() now returns a lazy spec ({ label, cmd, args, opts }). runTask(spec) spawns and returns { promise, kill, label }. phase() prints the banner and task list first, then calls runTask() on each spec before awaiting. Output order is now always correct: header → task completion lines.

The task list in the banner is also reformatted: previously one long comma-separated line (sprites, deploy-common, deploy-html, ...); now word-wrapped at 80 columns with · separators.

🐛 Flush stderr of killed tasks on sibling failure

Why: When a phase task fails, phase() sends SIGTERM to all sibling processes. Killed tasks resolved with code: -1 but their buffered stderr was discarded — if the killed task had begun emitting a useful error before being killed, that output was lost.

What changed: One line in runTask(): stderrBuf is now flushed in the signal branch of child.on('exit'), matching the existing flush in the non-zero exit branch.


CI fix — e2e workflow switched from grunt to build-pipeline.js (2026-06-18)

PR #91 (merged to main) added .github/workflows/e2e.yml with a build step that calls grunt. This PR removes grunt. The two collide: e2e.yml would fail with grunt: command not found the moment #109 landed.

Fix: merged origin/main into the branch (clean, no conflicts — e2e.yml was a new file) then updated the build step:

  • Dropped: npm install -g grunt-cli and grunt
  • Added: node scripts/build-pipeline.js and SKIP_MOBILE: '1'
  • Unchanged: BUILD_ROOT, THEME, PRODUCT_VERSION env vars; the version-read step; the overlay build step; all triggers and Playwright run steps

SKIP_MOBILE: '1' is correct here — the e2e suite runs Playwright on Chromium desktop only; the framework7 mobile editors are not exercised, so skipping them saves ~130s of CI time per run.

Bonus: build-pipeline.js runs the Tier-1 gates (verify-replacements preflight + verify-bundles/verify-deploy final gate). The e2e job now fails fast on a broken shim, unreplaced token, or missing output artifact — before Playwright spins up the nightly image. Cheaper failure, earlier signal.


What changed

Six webpack configs replace r.js AMD bundling. Six Node.js scripts replace all grunt asset tasks. A parallel orchestrator (build/scripts/build-pipeline.js) replaces the sequential Makefile chain. Grunt, Gruntfile.js, and all grunt test fixtures are removed.

Pipeline overview

Phase 1 (all parallel): sprites, deploy-common, deploy-html, deploy-reporter, deploy-theme-images, deploy-embed, webpack ×6, mobile ×4

Phase 2 (after Phase 1): deploy-resources, deploy-theme-img, inline-svgs

Local builds get webpack filesystem cache: 19s cold → 0.6s warm rebuild per editor (full pipeline wall-clock is dominated by other Phase 1 tasks).

Theme LESS overrides

grunt's appendThemeFiles() injected theme/euro-office/assets/less/theme.less as a top-level LESS entry. webpack equivalent:

  1. theme.config.mjs writes a one-line redirector stub at apps/common/main/resources/less/_theme-main.less
  2. Each editor's app.less imports the stub at the end

The stub is a redirector (not a copy) so theme.less's own relative @import "overrides.less" resolves from the theme directory. Adding a new theme override is theme-only — no build changes required. See docs/theme-less-overrides.md.

CSS outputs to apps/<editor>/main/app.css (root of main/), matching grunt's baseline so all url() references in LESS resolve at the correct depth.

Gotchas worth knowing

These surfaced during migration and are documented in docs/webpack-migration.md:

  • mangle: false is load-bearing — 117 files use var Common = Common || {} as a namespace guard that breaks under name mangling in webpack's module scope. Not negotiable without a larger refactor.
  • PRODUCT_VERSION must be ≥ 6 — EuroOffice rejects editors below version 6. The common.json fallback is 4.3.0. Pipeline fails fast if it's missing.
  • locale.js crashes webpack's AMD parser — fixed with string-replace-loader. Do not use noParse.
  • deploy-html + inline-svgs are a unit — running deploy-html alone leaves broken ?__inline=true tags and produces blank editors.
  • SVG sprites are baked into HTML at build time — no XHR requests for icons at runtime. index.html (un-built source) still uses SVGInjector but that file is never deployed.
  • Service worker caches aggressively — always test in incognito.
Known pre-existing issues (not caused by this migration)
Issue Notes
warnings_s.svg 404 CSS url() path depth — pre-existing
Transitions panel icons blank CSS classes not updated for SVG migration
FormsTab.getView() throws on plain PDF Upstream OnlyOffice bug, SDK catches it
Large locale diffs CI translation-merge step — expected noise, not regressions

Tracked follow-ups

DocumentServer changes

Corresponding Makefile update (removes grunt from make web-apps-dev, calls build-pipeline.js): see Euro-Office/DocumentServer PR [linked below].

moodyjmz and others added 30 commits June 13, 2026 16:01
IE11 reached EOL June 2023. This removes all IE compatibility output:
- Delete fix-ie-compat.js polyfill (fetch shim)
- Remove terser:iecompat and babel task configs from Gruntfile.js and appforms.js
- Remove grunt-babel loadNpmTasks
- Remove iecompat copy task from common.json
- Collapse all isIEBrowser require/app-loading branches in *.html.deploy to
  unconditionally load app/code bundles (12 HTML files across 6 editors)
- Remove isIEBrowser code_path ternaries in app.js (6 editors)

Expected baseline diff: 12 ie/*.js files absent, fix-ie-compat.js absent,
built index.html content changes. Re-baseline after a clean build.

Note: develop/setup/Makefile still references --skip-babel for web-apps-dev
target; that flag is now a no-op and will be removed in a parent-repo commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Re-captures build output baseline after:
- Step 4.0: IE compat bundles removed (12 ie/*.js files gone,
  fix-ie-compat.js gone)
- Upstream: terser mangling disabled (bundle sizes increased)

New state: 16,706 files, 20 JS bundles (was 32), self-diff PASSED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…magemin

- Update NODE_VERSION from stale 16 to 20 (matches bake Dockerfile)
- Add THEME=euro-office to grunt step (previously unset; CI was building
  without euro-office branding tokens applied)
- Add --skip-imagemin to match baseline (captured on ARM64/skip-imagemin;
  imagemin changes PNG sizes by 20-50%, which breaks the ±15% size gate)
- Add "Baseline diff" step: diffs grunt output against build/scripts/baseline.json,
  fails the build if any bundle drops modules or expected files go missing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
bower.json and .bowerrc have no effect — all listed packages
(backbone, underscore, jquery, requirejs, etc.) are already in
web-apps/vendor/ and managed directly. Bower is not installed or
referenced anywhere in the build pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Dead code on Linux — the win32 block that called chcp.exe to detect
the console codepage and load iconv-lite has never run in this
environment. iconv_lite stays declared (used by _encode/_themVal)
but will remain undefined, making _encode a no-op as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Documents that sdkjs is the only sibling-repo dep for the web-apps
Grunt build (core/dictionaries/document-templates are not referenced
here). Adds a preflight IIFE at Gruntfile startup that fails loudly
if sdkjs is missing, rather than silently producing a broken build.

Also documents the imagemin baseline gap: CI/dev baselines are
captured with --skip-imagemin; x86 production bake runs imagemin and
must not be diffed against the same baseline.

webpack migration note in docs/multi-repo-deps.md: sdkjs is the only
resolve.alias needed; a missing alias produces a smaller bundle with
no webpack error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Build numbers incremented by grunt increment-build during baseline
re-capture runs. NC assistant SVG icon updated to stroked outline style
(fill:none, stroke:currentColor) to match Nextcloud design language.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- build/scripts/baseline.js: module-manifest + hash + file-presence diff
  script referenced by CI gate (accidentally omitted from Step 1 commit)
- build/scripts/smoke-test.md: manual smoke test checklist per editor
- build-system-report.md: full build system analysis (Step 1 output)
- migration-plan.md: webpack 5 migration plan (Option D) v5
- docs/adr/0001-accessibility-phase-1-contracts.md: ADR for a11y contracts
- docs/release-process-report.md: release process analysis
- docs/build-system-report.md: deep-dive findings from Step 2 (grunt-inline
  internals, mobile webpack contract, r.js config shape, LESS var taxonomy)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Critical corrections from advisor review:
- Baseline gate is wrong for a bundler swap — module-manifest diff will
  always be red after migration; switch to file-presence + smoke test for
  migrated editors
- ~19 named define('id',…) modules: webpack ignores AMD module ids,
  resolves by file path — must audit and add each as resolve.alias
- code.js: second webpack entry with dependOn:'app', NOT dynamic import
- require.config() in HTML must be stripped — throws at runtime without require.js
- grunt-inline: keep as post-webpack pass (a), NOT HtmlWebpackPlugin
  templateParameters (loses watch, breaks relative-path resolution)
- AGPL license headers: TerserPlugin strips comments by default — must
  configure extractComments:false + preserve AGPL/Copyright comments
- Added four new risk table rows for above issues

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>
Keep only code, scripts, and build artefacts in the repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Two entries: app (LESS + AMD app.js) and code (app_pack.js with
dependOn:'app'). resolve.alias mirrors the r.js paths config;
externals covers the empty: paths (sdk, socketio, xregexp etc).

text! AMD plugin handled via NormalModuleReplacementPlugin +
asset/source rule for .template files.

TerserPlugin configured with extractComments:false and
comments:/AGPL|Copyright|Ascensio/ to preserve license headers.

SPIKE VALIDATION ITEM: require([...], callback) in entry files
may produce async chunks — verify at runtime or add eager-require
preprocessor if chunk names break the packaging contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Three issues found and resolved:

1. locale.js:174 — require([polyfill], cb) inside a non-AMD IIFE body
   crashes the AMD parser (addPresentationalDependency TypeError). The
   polyfill path is dead in modern browsers (native fetch/Promise).
   Fix: module.noParse for locale.js.

2. LaunchController.js:46 — require({waitSeconds:0}, dynamicArray, cb) —
   require.js 3-arg form with computed deps. webpack AMD parser cannot
   handle it. In webpack world DocumentServer loads code.js separately.
   Fix: parser:{amd:false} rule for LaunchController.js.

3. AMD require([...], callback) in entry files produced async chunks
   (742.chunk.js etc). DocumentServer packaging copies only app.js/code.js
   by exact name; stray chunk files are silently missing from the package.
   Fix: output.asyncChunks:false merges all lazy AMD chunks into their
   parent entry.

Result: app.js (1.04MB) + code.js (141KB), no stray chunk files.
AGPL header preserved via TerserPlugin comments filter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…k bundle

Two issues prevented runtime loading of the webpack-compiled visioeditor:

1. AMD externals compiled to `module.exports = void 0`.
   Root cause: per-entry `library: { type: 'amd' }` + per-external
   `{ amd: 'sdk' }` objects don't interact correctly — webpack cannot
   generate AMD define() deps from the multi-format external object in
   this config combination.
   Fix: global `externalsType: 'amd'` with plain string values.
   Result: `define("app", ["sdk"], (sdk) => {...})` — sdk is in the
   AMD deps array and the factory parameter is wired to the module stub.

2. require.js pre-config in index.html.deploy had no `paths` for
   external AMD modules (sdk, socketio, xregexp, allfonts, coapisettings,
   api). Without paths, require.js cannot resolve the sdk dep declared in
   the webpack define() wrapper before calling the factory.
   Fix: add `baseUrl` and `paths` to the `var require = {...}` pre-config.
   This mirrors the require.config() call that was in app.js source and
   was a no-op in webpack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Runtime-validated in the eo container: document editor loads, SDK
resolves, Backbone.Application boots.

Identical pattern to visioeditor spike with three differences:
- SDK path: sdkjs/word/sdk-all-min
- sdk shim added to require pre-config (sdk depends on allfonts,
  xregexp, socketio — require.config() in source is a no-op in
  webpack so the shim must live in the HTML pre-config)
- api is also a live external (documenteditor uses it directly;
  appears in define('app', ['api','sdk'], ...) wrapper)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
… presentation editors

Same pattern as documenteditor (runtime-validated). SDK paths:
- spreadsheeteditor: sdkjs/cell/sdk-all-min
- presentationeditor: sdkjs/slide/sdk-all-min

Both emit define("app",["api","sdk"],...) — api is a live external
in both editors, same as documenteditor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Scans emitted .js and .css output for unreplaced {{TOKEN}} mustache strings.
DefinePlugin cannot reach tokens inside string literals; any survivor is an
absolute build failure (no baseline needed). .json excluded — locale files
use {{...}} for runtime i18n interpolation.

Confirmed against current webpack output: 14 tokens unreplaced across
documenteditor, spreadsheeteditor, and presentationeditor bundles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…-replace-loader

DefinePlugin rewrites bare AST identifiers only — it cannot reach tokens inside
string literals like '{{PUBLISHER_NAME}}'. All three editors shipped with 14+
unreplaced brand/config tokens baked verbatim into the bundles.

Fix: add themeReplacements(productVersion) to theme.config.mjs exporting a
string-replace-loader 'multiple' array. Apply to all .js files under APPS_ROOT
in each webpack config. Tokens resolved from env vars > config.json > stock
defaults, matching Gruntfile's _themVal precedence.

Verified clean with: node build/scripts/baseline.js ... --scan-tokens
Discovered and added {{HELP_CENTER_WEB_VE}} (visioeditor-specific, absent from
Gruntfile jsreplacements as visioeditor is not in Euro Office's active set).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Mirrors the baseUrl/paths/shim block added to index.html.deploy in the
Step 5 webpack migration. Without it, the loader entry point cannot resolve
AMD externals (sdk, socketio, xregexp, allfonts, coapisettings, api) because
require.config() in the webpack bundle is a no-op — require.js reads only the
var require = {...} pre-config block before module loading begins.

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>
- Fix output path: grunt and webpack both write to web-apps/deploy/web-apps/apps/
  (BUILD_ROOT defaults to ../deploy from web-apps/build/); previous path
  deploy/web-apps/apps was relative to GITHUB_WORKSPACE root and never existed.

- Add webpack step: builds documenteditor, spreadsheeteditor, presentationeditor
  in parallel (overwrites grunt output for those three editors). grunt continues
  to build all editors so pdfeditor/appforms/visioeditor remain covered.

- Add token scan: runs --scan-tokens after webpack builds to catch any
  unreplaced {{TOKEN}} strings in the webpack output.

- Replace baseline diff with capture+upload: baseline.json was captured from
  a grunt-only build inside the eo container. The dual-build output (webpack
  for 3 editors, grunt for the rest) has different module manifests for the
  webpack editors. Upload the fresh baseline as an artifact so it can be
  committed and the --diff step restored in a follow-up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Remove migration scaffolding (token scan, baseline capture/upload) from CI.
Migration validation tools (baseline.js, --scan-tokens) are local development
aids used during migration work; they are not permanent project infrastructure
and will be removed from the repo when migration is complete.

CI now contains only what the project needs going forward:
- grunt --skip-imagemin (all editors — pdfeditor, appforms, visioeditor)
- webpack in parallel for documenteditor, spreadsheeteditor, presentationeditor

The webpack step is the gate: it fails CI if any editor build errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Node.js 20 actions are deprecated; forced to Node.js 24 from June 16th 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
buttons.svg was deleted in 482ad6d ("remove unused icons"). The
commented-out <inline> tag was left as a tombstone but grunt-inline
does not respect HTML comments, causing a warning on every build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
This reverts commit 6c25d46.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Add require pre-config (baseUrl, paths) to index_loader.html.deploy
to match index.html.deploy and support AMD externals resolution.
Add webpack.visioeditor.js to the CI webpack step for parity with
grunt which builds all four editors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Runs all four webpack configs sequentially, captures build time (webpack
internal + wall-clock), asset sizes (raw + gzip), and module counts.
Outputs a markdown report suitable for baseline comparison.

Usage: node scripts/perf-report.js [--out <file>]

MIGRATION TOOL — remove at migration completion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…plates

Without an explicit 'app' entry in paths, requirejs resolved require(['app'])
against baseUrl ("../../" = /web-apps/apps/) rather than the editor's own
directory, causing a 404 on app.js at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Replace the four r.js/Grunt webpack.{editor}.js shims with proper webpack 5
ESM configs (.mjs). All four desktop editors (document, spreadsheet,
presentation, visio) now build via webpack 5 and have been runtime-tested.

Key changes:
- build/webpack.{de,sse,ppe,ve}.mjs: webpack 5 configs with AMD library
  output, string-replace-loader for source patching, asyncChunks: false,
  mangle: false (Backbone needs constructor names), drop_console gated on
  NODE_ENV=production
- build/theme.config.mjs: add two new string-replace rules —
  c_* object-literal constants and ALL_CAPS primitive constants (SCALE_MIN,
  MENU_SCALE_PART etc.) — both promoted to window globals so cross-file
  consumers survive webpack factory scoping; ^ anchor in multiline mode
  is critical to avoid hitting indented inside-define declarations
- apps/common/main/lib/controller/LaunchController.js: use window.require
  for post-launch AMD loading so require.js (runtime) is called, not
  webpack's compiled require
- apps/*/main/index.html.deploy × 4: add jquery path to require.js config
  (webpack externals declare jquery as AMD; require.js must know where to
  find it at runtime)
- apps/*/main/index_loader.html.deploy × 4: same jquery path addition
- build/scripts/perf-report.js: update EDITORS config references .js → .mjs
- .claude/: add migration-topology, findings (6 files), icon-migration, and
  other session notes; all parked issues and known gaps documented
- CLAUDE.md: add findings index, hard rules, and parked/known issues section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
onboarding.md, pr-reviews.md, pr77-fixes.md, mobile-build-index-html.md
are Claude operational notes with no value to other contributors.
findings/*.md, migration-topology.md and icon-migration.md stay committed
as developer documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@chrip chrip self-requested a review June 17, 2026 21:45

@chrip chrip left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing comments addressed (not repeating):

  • .gitignore .claude/ exclusion preference (juliusknorr)
  • CLAUDE.md parked issues scope (juliusknorr: lines 36, 40, 52)
  • Vendored dependencies in vendor/ (juliusknorr: apps/documenteditor/forms/index.html.deploy:266)

Summary: Solid migration — removing grunt and IE baggage is the right move. Four findings below, one being a real bug in CI (sequential steps lose the claimed speedup), two are fragility issues that will bite later, and one cleanup.


1. CI doesn't use build-pipeline.js — claimed speedup won't materialize
.github/workflows/build.yml

The PR body says "CI build time: ~7 min → ~2 min" but the CI workflow runs all steps sequentially, duplicating the pipeline logic in shell. The parallelism benefit only exists in build-pipeline.js. Either replace the individual steps with a single node scripts/build-pipeline.js step, or use GitHub Actions jobs with needs: for parallel execution. As-is, CI won't see the claimed improvement.


2. inline-svgs.js treats zero substitutions as a hard error
build/scripts/inline-svgs.js line 118

if (totalSubstitutions === beforeEditor) {
    process.exitCode = 1;
}

Any future template change that removes inline tags (e.g., switching to bundled sprites) will silently break the build with a confusing error. Downgrade to a warning, or make it opt-in per editor.


3. Hardcoded baseUrl: "../../" in HTML templates
apps/documenteditor/main/index.html.deploy line 416 (and all other editor templates)

baseUrl: window.customBaseUrl || "../../"

This assumes a fixed deploy layout. The old code relied on implicit requirejs baseUrl resolution, which tolerated different paths. If the app is served from a different subdirectory and customBaseUrl is never set, requireJS will fail to resolve all modules. Consider computing baseUrl dynamically from document.location.href.


4. Dead vendor artifacts: fetch and es6-promise still deployed
build/scripts/deploy-common.js simpleVendors array

fix-ie-compat.js was deleted and IE conditional loading removed from HTML. The fetch and es6-promise polyfills are no longer loaded by any code path, but deploy-common.js still copies them to BUILD_ROOT. Not harmful, but unnecessary deployed bytes.

Assisted-by: OpenCode:Qwen3.6-27B

CLAUDE.md was the only Claude-related file committed in the repo; the
.claude/ dir is already gitignored. Per review (juliusknorr), Claude
guidance files should not live in the shared repo. Untrack and gitignore
CLAUDE.md so it persists locally for the Claude workflow but is not shipped.

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 a team as a code owner June 18, 2026 11:18
@moodyjmz moodyjmz requested review from emberfiend and rikled and removed request for a team June 18, 2026 11:18
moodyjmz and others added 2 commits June 18, 2026 15:41
grunt is removed by this branch. e2e.yml (from #91) called grunt —
the two would collide on merge. Replace with the webpack pipeline.

Drop: npm install -g grunt-cli, grunt
Add:  node scripts/build-pipeline.js, SKIP_MOBILE=1

SKIP_MOBILE: e2e suite is Playwright/Chromium desktop-only; no mobile
editors are exercised, so skipping framework7 builds saves ~130s CI time.

Tier-1 gates (verify-replacements, verify-bundles, verify-deploy) now
also run inside this job — faster failure signal before Playwright spin-up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz

moodyjmz commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

Thanks for all four findings — responses inline.

Finding 1 (CI steps vs build-pipeline.js): Valid. Will switch build.yml to call node scripts/build-pipeline.js directly in the same way e2e.yml already does. Coming in the next commit.

Finding 2 (inline-svgs zero-substitutions as hard error): Intentional. Zero substitutions on a specific editor means the HTML templates didn't contain the expected inline tags — that produces blank editors (documented in the findings catalog: a missed inline pass is "Not supported version" / blank page, high blast radius). The script already has a precedent for the softer path: device_scale.js missing gets a warning and a skipped tag, because that miss is benign. The per-editor hard error exists precisely because this miss is not benign. Making it a warning would silently defeat the gate.

Finding 3 (hardcoded baseUrl: "../../"): Pre-existing from the grunt templates — the webpack migration didn't change it. Worth noting though: it's not actually hardcoded without an escape — every template uses window.customBaseUrl || "../../", so a deployer serving from a non-standard path already has a runtime override hook. The embed templates use a bare "../../" but those are fixed-layout contexts.

Finding 4 (fetch / es6-promise "dead" bytes): You're right that they're not loaded by any IE conditional bundle — that's gone. But they're not dead: apps/common/locale.js:170 does a live window.fetch feature-detect on every editor load and dynamically require()s both polyfills if the browser lacks native fetch:

if ( !window.fetch ) {
    var polyfills = ['../vendor/fetch/fetch.umd'];
    if ( !window.Promise ) {
        require(['../vendor/es6-promise/es6-promise.auto.min'], ...);
    } else require(polyfills, _requireLang);
} else _requireLang();

So removing them from the manifest would break localisation on any browser that hits that path.

That said — you've raised a valid broader point. The browsers that lack native fetch are IE 11 (EOL June 2022) and Firefox 31–38 (2014–2015, current FF is 151). The browserslist still declares Firefox >= 31. There's no good reason to keep supporting Firefox 31 in 2026. Properly removing these polyfills means: raise the browser floor in the browserslist (suggest Firefox ESR so it tracks dynamically), remove the locale.js feature-detect guard, then drop the manifest entries. That's a product-level browser support decision though, not a build system change, so it's out of scope for this PR.

Filed as a follow-up: #129

moodyjmz and others added 4 commits June 19, 2026 16:02
12 individual steps (sprites, deploy-*, webpack×6, mobile×4, verify×2)
replaced with a single call to build-pipeline.js, matching e2e.yml.

The pipeline runs all Phase 1 tasks in parallel (deploy + webpack + mobile
together), compared to the old workflow which ran them in sequential groups.
Env handling (BUILD_ROOT validation, PRODUCT_VERSION guard, BUILD_NUMBER
fallback) moves into the pipeline; the shell 'Resolve build paths' step
is removed. Translation merge kept as a separate step — not in the pipeline.

BUILD_ROOT set explicitly to ${{ github.workspace }}/web-apps/deploy.
PRODUCT_VERSION inherited from job-level env (9.2.1).
THEME: euro-office passed to pipeline.

Addresses chrip's finding 1 on PR #109.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
- Reframe CI time headline: speedup is webpack vs r.js + Phase 1
  parallelism, not a single cause
- Document the 2026-06-18 build.yml → build-pipeline.js change:
  rationale, what collapsed, what stayed separate (translation merge)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
New files:
  build/replacements.manifest.mjs  — single source of truth for 6
    load-bearing string-replace-loader patterns + baselines
  build/vendor.manifest.mjs        — single source of truth for vendor
    artifact src/dest paths
  build/scripts/verify-replacements.mjs  — Check A: preflight pattern audit
  build/scripts/verify-bundles.mjs       — Check B: post-build token scan
  build/scripts/verify-deploy.mjs        — Check C: artifact existence gate

Modified:
  build/theme.config.mjs           — themeReplacements() pulls from manifest
  build/webpack.editor.factory.mjs — locale rule uses manifest entry
  build/scripts/lib/build-utils.js — copyFile gains { required: true } mode
  build/scripts/deploy-common.js   — imports VENDORS manifest; generic loop
    replaces per-vendor functions; deployRequireJS/deployMonaco accept entry arg

These were implemented and validated locally but never committed.
CI failure on build-pipeline.js switch exposed the gap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
…p safety, reporter mangling

Bump common.json version to 9.2.1 to match the PRODUCT_VERSION stamped by
CI; clear the skip-worktree flag so the canonical version is tracked in git.
Previously common.json had 4.3.0, causing local builds without an explicit
env var to fail the PV_MAJOR >= 6 guard immediately.

Move sharp require inside writeRaster() rather than at module top-level.
sharp is a native addon (libvips) — on arm64, Alpine, or any runner without
a prebuilt binary, the top-level require throws MODULE_NOT_FOUND before any
function runs, crashing deploy-common, deploy-resources, and deploy-reporter
even when no image processing is needed.

Wrap o.kill() in try/catch in the phase() abort loop. When a fast task exits
before a slow sibling fails, child.kill() calls process.kill() on an already-
reaped PID and throws ESRCH on Linux. The throw propagated out of Promise.all,
replacing the real failure message with "Fatal: kill ESRCH".

Remove mangle:false from deploy-reporter.js. With it set, Terser only strips
whitespace and leaves identifiers intact, producing a bundle ~30-50% larger
than necessary. This is a plain Terser call on a single file — not webpack
module scope — so mangling is safe. mangle:false in webpack.editor.factory.mjs
is unchanged; it remains load-bearing for the Common namespace guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz force-pushed the build/webpack-migration branch from 226121e to 41fcd47 Compare June 22, 2026 11:26
moodyjmz and others added 2 commits June 22, 2026 16:12
…ess cause; harden verify-bundles gate

- docs/webpack-migration.md gotcha #8: expand SW staleness note to explain
  WHY incognito is required locally — PRODUCT_VERSION is pinned in the eo
  Makefile so local rebuilds emit app.js at the same URL with no cache_tag
  change; SW serves the stale bundle. Production deploys self-bust via the
  version-prefix redirect + flush-cache.sh.

- docs/webpack-migration.md gotcha #9: add invariant note — output.clean:false
  is safe only because the output set is fixed. Enabling splitChunks, async
  import(), or asyncChunks without content-hashed filenames or per-editor
  cleaning leaves stale chunks on disk that the SW will cache silently.

- build/webpack.editor.factory.mjs: matching inline comments at clean:false and
  splitChunks:false cross-referencing the invariant and the Common scoping
  finding, so the constraint is visible at the call site without needing to
  consult docs separately.

- build/scripts/verify-bundles.mjs: hard-fail (not warn) when BUILD_ROOT exists
  but an expected bundle is absent. Previously warn-and-continue allowed a
  partial build to pass the token-survivor gate; each gate should be
  self-sufficient even though verify-deploy also checks bundle existence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
CSS hashes in mobile index.html files updated to reflect esbuild output
from the TerserPlugin → EsbuildPlugin switch. Build artifacts — no
functional change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@chrip chrip self-requested a review June 24, 2026 15:14

@chrip chrip left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Resolved

  • CI sequential steps (chrip Finding 1) — fixed in a4d8a5b0 via build-pipeline.js
  • .claude/ and CLAUDE.md added to .gitignore, file removed from commit (juliusknorr)

🔴 Blocks merge

1. Missing Assisted-by: trailers — all 51 commits

Every commit has Co-Authored-By: Claude Sonnet 4.6 but none have the Assisted-by: trailer required by AGENTS.md. These are separate conventions; Co-Authored-By satisfies GitHub attribution but does not fulfil the policy requirement.

See AGENTS.md: "Add an Assisted-by: AGENT_NAME:MODEL_VERSION git trailer to every commit containing AI-assisted content."

Please rebase before merge. A one-liner covers all commits:
git rebase origin/main --exec 'git commit --amend --no-edit --trailer "Assisted-by: ClaudeCode:claude-sonnet-4-6"'

2. build/scripts/inline-svgs.js:126 — zero substitutions = hard build exit

if (totalSubstitutions === beforeEditor) {
    process.exitCode = 1;
}

This breaks CI the moment a template legitimately stops using inline SVG tags. Downgrade to console.warn, or add a --strict flag for pipelines that want the hard exit explicitly.


🟡 Should fix

3. Hardcoded baseUrl: "../../" in HTML deploy templates (index.html.deploy:416)

baseUrl: window.customBaseUrl || "../../",
Assumes a fixed deploy-directory depth. Suggest deriving from document.location.href at runtime, or documenting the constraint so operators know customBaseUrl is required in non-standard deployments.

4. Dead vendor artifacts (build/vendor.manifest.mjs:41–42)

fetch and es6-promise remain in the manifest after fix-ie-compat.js was deleted and IE paths removed from HTML. ~30 KB of unreachable bytes — please remove both entries.

5. Stale grunt references (build/README.md:248, 420)

Both lines still reference grunt --skip-imagemin. Rephrase as past tense or remove the command; the surrounding explanatory context can stay.

6. mobile:install outside Phase 1 banner (build/build-pipeline.js:257–273)

mobile:install runs before the Phase 1 banner; its dependents appear inside Phase 1. Output ordering is misleading in CI logs. Move mobile:install inside the phase or add a named pre-phase.


ℹ️ Investigated, not an issue

LaunchController.js window.require — code is window.require({waitSeconds: 0}, …), not a bare call; config is intact; correct.
scroller.less data-uri replacements — all 13 replacements correct; data-uri() is unavailable in webpack's less-loader.
o.kill() try/catch — adequately handles ESRCH from already-exited processes.

Assisted-by: ClaudeCode:claude-sonnet-4-6

@moodyjmz

Copy link
Copy Markdown
Member Author

⚠️ Depends on #140 — fold in before/with this migration, or the #89 branding fix regresses

The ONLYOFFICE-branding fix (#139) ships on the grunt main line. Its source edits (Main.js gate, .deploy loader logo/title tokens) carry across a merge for free. The build-wiring half does not:

  • Token substitution for {{APP_TITLE_TEXT}} / {{LOADER_LOGO}} / {{LOADER_LOGO_DARK}} in the editor HTML lives in Gruntfile.js replace:indexhtml on the grunt side.
  • This migration deletes Gruntfile.js. On the webpack side that logic must live in build/scripts/deploy-html.js, which today substitutes only @@SRC_ROOT@@.

If the migration lands without it: Gruntfile.js is gone, the tokenized .deploy files survive from #139, deploy-html.js doesn't know the new tokens → every editor's loader ships src="{{LOADER_LOGO}}" and <title>{{APP_TITLE_TEXT}} …. #89 silently un-fixes itself the moment webpack becomes main.

#140 (parked draft, base build/webpack-migration) is exactly that deploy-html.js change. Merge it into this branch — or cherry-pick its deploy-html.js commit — before this PR lands. Flagging here so it doesn't fall through the branch gap.

🤖 Generated with Claude Code

moodyjmz added 2 commits June 29, 2026 10:11
package.json had grunt stripped in f0017ae but package-lock.json
was never updated — npm ci failed for anyone on this branch.

Lock file now reflects the webpack-only dependency set.

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Resolves conflicts from branding PRs (#89, #136) landing on main
after build/webpack-migration diverged:

- presentationeditor/index.html.deploy: take {{APP_TITLE_TEXT}} token
  and loader logo changes from main; keep webpack require paths/baseUrl/
  shim and drop IE compat block from webpack branch
- spreadsheeteditor/index.html.deploy: same — also keeps the
  "Spreadsheet Editor" title fix from main
- build/Gruntfile.js: keep deletion (webpack branch) — grunt is gone;
  main's tweaks to it are irrelevant to the webpack pipeline

Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>

@Aiiaiiio Aiiaiiio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it, works well

@moodyjmz moodyjmz merged commit 74a13d1 into main Jun 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build process related changes dependencies enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build: evaluate esbuild as TerserPlugin replacement for mobile (framework7-react) builds

4 participants