JavaScript port: emit a bundle that is ready to deploy as-is#5466
Conversation
JavaScriptPort.jar carries webapp/ so off-repo builds can find port.js, and the translator copies webapp/assets into every app it builds. That meant ~3.6MB of the port test app's own fixtures -- leather.res (2.1MB), chrome.res (778KB), video.mp4 (666KB), Handlee-Regular.ttf, Page.html, test.json -- landed in every JavaScript app's public web root. None of them is referenced by any class in the port or in CodenameOne core. Exclude them from the jar. They stay in the source tree, so in-repo builds and the javascript-screenshots suite still see them. The system themes HTML5Implementation.getNativeTheme() resolves (iOS7Theme.res, iPhoneTheme.res, iOSModernTheme.res, android_holo_light.res, tzone_theme.res) and CN1Resource.res are kept. Also delete the jar before rebuilding it: Ant's jar task treats an existing archive as up to date when it is newer than every input file, so on an incremental build a change to this include/exclude list is silently ignored and a stale jar ships. That cost a full debugging cycle here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reduces the default output size of JavaScript builds by preventing the JavaScript port’s demo/test fixture assets from being bundled into JavaScriptPort.jar, which the translator then propagates into every JavaScript app’s public web root. It also makes the jar packaging step robust for incremental builds by forcing the jar to be recreated when include/exclude rules change.
Changes:
- Delete the previously-built
JavaScriptPort.jarbefore running Ant’s<jar>task to avoid stale archives on incremental builds. - Exclude large demo/test fixture files (e.g.,
leather.res,chrome.res,video.mp4) from thewebapp/assetscontent that gets embedded intoJavaScriptPort.jar.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| The system themes assets/ must keep are the ones | ||
| HTML5Implementation.getNativeTheme() resolves: | ||
| iOS7Theme.res, iPhoneTheme.res, iOSModernTheme.res, | ||
| android_holo_light.res and tzone_theme.res. --> |
There was a problem hiding this comment.
Correct, and already fixed — that comment was rewritten a few commits ago. It now names installNativeTheme() (for what the bundle must contain) and resolveNativeThemeResource() (for what picks between them at runtime). No getNativeTheme() left in the tree.
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
assets/theme.res is the port test app's theme (1.4MB). Real apps ship their own theme.res at the bundle root and getResourceAsStream() deliberately prefers the root copy for theme.res / CN1Resource.res, so this one is never read -- confirmed against a running app, whose request log shows the root theme.res fetched and this one never touched. Shipping it also kept a shadowing hazard alive for no benefit. assets/tzone_theme.res is returned by no code path: getNativeTheme() only ever yields iOS7Theme / iPhoneTheme / iOSModernTheme / android_holo_light / AndroidMaterialTheme / androidTheme. The single mention of tzone_theme in the port is inside a comment, which is exactly why it survived the first pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
maven/parparvm/pom.xml:242
- The comment listing the “system themes assets/ must keep” includes
tzone_theme.res, but the JS port’s theme selection doesn’t return that resource. InPorts/JavaScriptPort/.../HTML5Implementation.java,resolveNativeThemeResource()returns only iOS7/iPhone/iOSModern/AndroidMaterial/android_holo_light/androidTheme (see ~4092-4147), so this list is misleading and contradicts the later exclusion ofassets/tzone_theme.res. Update the comment to reflect the actual set of themes that may be loaded.
The system themes assets/ must keep are the ones
HTML5Implementation.getNativeTheme() resolves:
iOS7Theme.res, iPhoneTheme.res, iOSModernTheme.res,
android_holo_light.res and tzone_theme.res. -->
maven/parparvm/pom.xml:261
- This exclusion rationale refers to
getNativeTheme(), but there is nogetNativeTheme()in the JavaScript port; theme selection is performed byHTML5Implementation.resolveNativeThemeResource()(called frominstallNativeTheme()). Updating this comment will avoid confusion when someone tries to audit which themes must remain bundled.
<!-- No code path returns tzone_theme.res: getNativeTheme()
only ever yields iOS7Theme / iPhoneTheme /
iOSModernTheme / android_holo_light /
AndroidMaterialTheme / androidTheme. The one mention of
tzone_theme in the port is inside a comment. -->
I had described iOS7Theme/iPhoneTheme/android_holo_light as "the themes getNativeTheme() resolves" and listed iOSModernTheme among them. That misread installNativeTheme(): the JS port defaults to the LEGACY pair (android_holo_light on an Android user agent, iOS7Theme elsewhere) so existing screenshot baselines stay comparable, and the modern themes are opt-in via ios.themeMode / and.themeMode / nativeTheme / javascript.native.theme. The modern .res files live in Themes/ and are staged per target, not from webapp/assets. No functional change: the same files are excluded and the same ones kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
maven/parparvm/pom.xml:243
- PR description says the bundle keeps
tzone_theme.resandtheme.res, but this POM change explicitly excludes both (assets/theme.resandassets/tzone_theme.res). Either the description/kept list is outdated, or these excludes are too aggressive. Please align the PR description and/or the inline comment here so it’s clear which themes/resources are intentionally retained vs excluded (and why).
Kept: the native themes installNativeTheme() can open
from the bundle. The JS port defaults to the LEGACY
pair (android_holo_light.res on an Android user agent,
iOS7Theme.res elsewhere) so existing screenshot
baselines stay comparable; iPhoneTheme.res is the
HTML5Implementation.resolveNativeThemeResource() can return six themes -- iOS7Theme, iPhoneTheme, iOSModernTheme, androidTheme, android_holo_light, AndroidMaterialTheme -- but the bundle only carried three of them, so setting ios.themeMode / and.themeMode / nativeTheme / javascript.native.theme to anything modern (or to legacy on Android) hit the catch in installNativeTheme() and silently fell back to the legacy theme. The modern themes postdate the old TeaVM port; the ParparVM port never grew the wiring the other targets have. The build cannot pick one of the six: resolveNativeThemeResource() decides at RUNTIME from the browser user agent, so "modern" means iOSModernTheme in an iOS-like browser and AndroidMaterialTheme everywhere else. Ship the whole set instead. Take them from Themes/, the single source of truth kept in sync from the CSS sources by native-themes-sync.yml, the same way maven/ios folds iOSModernTheme.res into nativeios.jar and maven/javase copies the set onto its classpath -- rather than from the port's own copies under webapp/assets, which drift: the webapp's iPhoneTheme.res was already a stale version of the Themes/ one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Scope grew after review feedback — this now fixes a real bug, not just bloat. The modern native themes were never wired up on the JavaScript port. They postdate the old TeaVM port, and the ParparVM port never grew the staging the other targets have. Note the build cannot stage just the one an app needs: They now come from Net size: the six themes add ~780KB versus the three stale copies, and removing the demo fixtures takes ~3.6MB off, so a JavaScript bundle still ends up substantially smaller than before. I also closed #5468, which I had filed claiming the modern path was broken by default. That was wrong — legacy is the intended default, to keep screenshot baselines comparable. The real defect is the one fixed here: the opt-in never worked. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
maven/parparvm/pom.xml:263
- This change excludes assets/tzone_theme.res from the shipped JavaScriptPort.jar, but (a) the PR description says it is kept, and (b) apps can still explicitly request it via Display property javascript.native.theme. With this exclusion, that explicit selection will now fall back to the legacy theme at runtime.
<!-- No theme-resolution path yields tzone_theme.res; the one
mention of it in the port is inside a comment. -->
<exclude name="assets/tzone_theme.res"/>
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 144 screenshots: 144 matched. |
|
Compared 149 screenshots: 149 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
The bundle is meant to be unpacked straight into a web root, but two things made every consumer post-process it first. Zip the dist FLAT. zipDirectory() was passed the dist directory's own name as the zip root, so everything landed under "<MainClass>-js/" and had to be flattened before it could be served. The old cloud/TeaVM bundle was flat, so this also lets one deployment script handle both. Stop copying build descriptors into the bundle. ByteCodeTranslator copies every non-class input file into the output by basename, so a Maven-built app leaks META-INF/maven/**/pom.xml -- dependency list and all -- plus pom.properties and MANIFEST.MF. Harmless for the iOS target, where the output is a source tree; for the JavaScript target that output IS a public document root. None of them is an application resource. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Two more build-side fixes, prompted by the fair question of why a consumer should have to post-process the bundle at all. The JavaScript build is supposed to hand back something deployable as-is, and it wasn't. 1. The zip is now flat. 2. Build descriptors no longer leak into the bundle. Both were being worked around downstream; that workaround is now deleted. Two things I deliberately did not change, since they look intentional and you'd know better:
Verified end to end after both changes: the bundle unzips straight into a web root, the console renders, all APIs return 200, and a request log from the running app matches the shipped file list exactly. |
| * @param i source | ||
| * @param o destination | ||
| */ | ||
| /** | ||
| * Build descriptors that ride along inside an application jar's META-INF | ||
| * but are not application resources. Every non-class input file is copied | ||
| * into the output by basename, so without this filter a Maven-built app | ||
| * leaks its pom.xml (dependency list and all), pom.properties and | ||
| * MANIFEST.MF into the bundle -- and for the JavaScript target that bundle | ||
| * IS a public document root. | ||
| */ | ||
| private static boolean isBuildMetadata(String name) { | ||
| return "pom.xml".equals(name) | ||
| || "pom.properties".equals(name) | ||
| || "MANIFEST.MF".equals(name); | ||
| } |
There was a problem hiding this comment.
Fixed. isBuildMetadata() now sits above the Javadoc block, so @param i / @param o reattach to copy(InputStream, OutputStream).
| Parser.parse(f); | ||
| } else { | ||
| if(!f.isDirectory()) { | ||
| if(!f.isDirectory() && !isBuildMetadata(f.getName())) { |
There was a problem hiding this comment.
Fixed — the predicate is path-aware now. It walks up the parents and only returns true when the file sits under a META-INF directory, so an app resource that happens to be named pom.xml is still copied.
| <!-- No theme-resolution path yields tzone_theme.res; the one | ||
| mention of it in the port is inside a comment. --> | ||
| <exclude name="assets/tzone_theme.res"/> |
There was a problem hiding this comment.
Option (a). tzone_theme.res is genuinely not shipped and no runtime path can select it — its only mention in the port is inside a comment. The PR description was stale; it has been rewritten and now lists the file among the excluded fixtures with that reasoning.
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 217 screenshots: 217 matched. |
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
Cloudflare Preview
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 147 screenshots: 147 matched. |
|
Compared 147 screenshots: 147 matched. |
|
Compared 181 screenshots: 181 matched. |
Three artifacts shipped in every JavaScript app that should not have.
video.js + RecordRTC (~944KB) were already meant to be opt-in. VideoJS is
only ever reached from HTML5Implementation.captureVideo(), which calls
VideoJS.init() behind a try/catch and, when it fails, logs "VideoJS is not
loaded, using default captureVideo behaviour. Add the
javascript.includeVideoJS build hint..." and falls back. The libraries are
lazy-loaded at runtime by ScriptTool. So the hint documented an opt-in that
nothing on the build side implemented: every app paid for the library
whether or not it captured video. Honour the hint.
samplerate.min.js (~485KB) is unconditional dead weight. Nothing loads it by
path; js/fontmetrics.js only probes for an already-defined Samplerate global
("use libsamplerate if it is available") and nothing ever defines one.
vm_protocol.md and jso-bridge-dispatch-ids.txt are developer artifacts, and
the translator's output directory becomes the app's PUBLIC web root.
vm_protocol.md documents the worker boundary and is checked in under
vm/ByteCodeTranslator/src/javascript/, so copying it into every deployed app
published documentation nobody reads from there.
jso-bridge-dispatch-ids.txt is a sidecar for the dispatch-id mangler, which
is opt-in and off by default (-Dparparvm.js.manglesigs=1); the class walk
that builds it is what the mangler needs, and nothing reads the emitted file
at all. Both now require -Dparparvm.js.diagnostics.
Note reachability cannot decide the media case: HTML5Implementation
references the media classes itself, so they survive culling even in an app
that plays and records nothing (HTML5MediaRecorder still had 129 references
in a console bundle). The build hint is the right gate.
The integration test opts into diagnostics so it still covers
vm_protocol.md; the core-slice test drops that assertion, since
translated_app.js already proves the translator ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| Parser.parse(f); | ||
| } else { | ||
| if(!f.isDirectory()) { | ||
| if(!f.isDirectory() && !isBuildMetadata(f.getName())) { |
There was a problem hiding this comment.
Fixed — see the reply on the other thread: the check now requires a META-INF ancestor, so only real jar metadata is skipped.
| private static boolean isBuildMetadata(String name) { | ||
| return "pom.xml".equals(name) | ||
| || "pom.properties".equals(name) | ||
| || "MANIFEST.MF".equals(name); |
There was a problem hiding this comment.
Fixed — the filter now requires a META-INF ancestor rather than matching on basename alone.
| if (emitDiagnostics()) { | ||
| writeProtocol(outputDirectory); | ||
| writeJsoBridgeManifest(outputDirectory, classes); | ||
| } |
There was a problem hiding this comment.
Fixed. emitDiagnostics() parses the value and accepts 1 or true (case-insensitive) instead of merely checking for presence, so -Dparparvm.js.diagnostics= no longer turns diagnostics on.
| static boolean emitDiagnostics() { | ||
| return System.getProperty("parparvm.js.diagnostics") != null; |
There was a problem hiding this comment.
Fixed — the property is parsed for 1/true now, matching the documented contract.
| <!-- No theme-resolution path yields tzone_theme.res; the one | ||
| mention of it in the port is inside a comment. --> | ||
| <exclude name="assets/tzone_theme.res"/> |
There was a problem hiding this comment.
Option (a): tzone_theme.res is genuinely not shipped, and the PR description was stale. Rewritten — the file is now listed among the excluded fixtures, with the reason: no theme-resolution path yields it, and its only mention in the port is inside a comment.
| <zipfileset dir="${project.basedir}/../../Themes" prefix="webapp/assets"> | ||
| <include name="iOS7Theme.res"/> | ||
| <include name="iPhoneTheme.res"/> | ||
| <include name="iOSModernTheme.res"/> | ||
| <include name="androidTheme.res"/> | ||
| <include name="android_holo_light.res"/> | ||
| <include name="AndroidMaterialTheme.res"/> |
There was a problem hiding this comment.
Same thread as above — description rewritten. tzone_theme.res is excluded on purpose and is now documented as excluded.
| // Zip the dist FLAT. The bundle is meant to be unpacked directly | ||
| // into a web root, so a "<MainClass>-js/" wrapper only forces every | ||
| // consumer to flatten it first. (The old cloud/TeaVM bundle was flat | ||
| // too, so this also keeps deployment scripts working across both.) | ||
| zipDirectory(distDir, jsOutputZip, null); |
There was a problem hiding this comment.
Agreed it is compatibility-affecting, and it is deliberate: removing that wrapper hop is the point of this change — the build should hand back a tree you can serve, not one you have to unwrap first. I have not made it configurable, because a toggle would leave both layouts alive forever and consumers would have to handle each.
Instead the PR description now opens with a unzip … && cp -R <MainClass>-js/. dest/) and the fix (drop the hop), so it can be lifted into the release notes.
| File samplerate = new File(js, "samplerate.min.js"); | ||
| if (samplerate.isFile()) { | ||
| samplerate.delete(); | ||
| } |
There was a problem hiding this comment.
Fixed for both deletions. samplerate.min.js checks the delete() return value, and the js/videojs tree is re-tested with exists() after delTree(). Either failure logs a WARNING: … it will ship in the bundle.
I kept it non-fatal deliberately: the consequence is a bundle carrying a dead file, which is not worth failing an otherwise good build over — but it will no longer happen silently.
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
- isBuildMetadata() is path-aware: it only skips pom.xml / pom.properties / MANIFEST.MF that sit under a META-INF directory, so an app resource that happens to carry one of those names is still copied into the bundle. - Move the helper above copy()'s Javadoc; inserting it in between had detached the @param i / @param o lines from the method. - emitDiagnostics() parses parparvm.js.diagnostics for 1/true rather than testing for presence, matching the documented contract. - Report a failed prune instead of silently shipping the file: both js/videojs and js/samplerate.min.js verify the deletion happened and log a warning if it did not. - Add the missing licence headers to JavascriptBundleWriter and JavascriptCn1CoreCompletenessTest (check-copyright-headers). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #5465 / #5457.
Since #5457,
JavaScriptPort.jarcarrieswebapp/so off-repo builds can findport.js, and the translator copies that webapp into every app it builds. That made the port's public web root the de facto payload of every JavaScript app — and it turned out to contain the port test app's own fixtures, a video-capture stack no app asked for, developer diagnostics, and the app jar's Maven descriptors.This PR makes the JavaScript build emit a tree you can deploy as-is: nothing in it is there by accident, and the zip unpacks straight into a web root.
Rebuilding a real app (the BuildCloud console) against it: 16 MB → 9.0 MB, with no change to what the app can do.
1. Native themes now actually work when you opt in
HTML5Implementation.resolveNativeThemeResource()picks a theme at runtime from the browser user agent plus theios.themeMode/and.themeMode/nativeTheme/javascript.native.themehints. The bundle only ever shipped three of the six themes it can resolve, so opting intomodernsilently fell back to the legacy theme instead of applying — the file simply wasn't there.All six now ship, staged straight from
Themes/— the single source of truth kept in sync from the CSS sources bynative-themes-sync.yml— the same waymaven/iosfoldsiOSModernTheme.resintonativeios.jarandmaven/javasecopies the whole set onto its classpath. The webapp's own copies are excluded because they drift: itsiPhoneTheme.resalready differs from the one inThemes/.Defaults are unchanged: with no theme hint the port still lands on
android_holo_light.resfor an Android user agent andiOS7Theme.reseverywhere else, so existing screenshot baselines stay comparable.2. Demo fixtures stay in the source tree
leather.restheme.res(the port test app's own)chrome.resvideo.mp4Handlee-Regular.ttftzone_theme.resPage.html,test.jsonNone is referenced by any class in the port or in CodenameOne core — I checked every name with fixed-string searches across
Ports/JavaScriptPort/src/main/javaandCodenameOne/src; zero hits. (Handlee-Regular.ttfappears only inside a Javadoc example inFont.java. The single mention oftzone_theme.resin the port is inside a comment — no resolution path yields it.)assets/theme.resis worth calling out separately: real apps ship their owntheme.resat the bundle root, andgetResourceAsStream()deliberately prefers the root fortheme.res/CN1Resource.res, so the webapp copy was never read (confirmed against a running app — the root one is fetched, this one never is) while keeping a shadowing hazard alive.They are excluded from the jar and stay in the source tree, so in-repo builds and the
javascript-screenshotssuite still see them.3. The media stack is opt-in, as its own error message always claimed
VideoJS.init()is called byHTML5Implementation.captureVideo()behind a try/catch that logs:…but nothing on the build side ever implemented that hint, so every app paid 944 KB of video.js + RecordRTC whether or not it captured video. The hint is now honoured (
javascript.includeVideoJS=trueto bundle them).samplerate.min.js(488 KB) is unconditional dead weight and is always dropped: nothing loads it by path.js/fontmetrics.jsonly probes for an already-definedSamplerateglobal ("use libsamplerate if it is available"), and no script tag,ScriptToolcall or bridge ever defines one.Reachability analysis can't gate either of these —
HTML5Implementationreferences the media classes itself — which is why it's a build hint.4. Developer diagnostics are behind a flag
vm_protocol.mdandjso-bridge-dispatch-ids.txtwere written into the app's public web root on every build.vm_protocol.mdis documentation of the worker/main-thread protocol. It is already checked in atvm/ByteCodeTranslator/src/javascript/; emitting a copy next toindex.htmlpublishes it to the app's users.jso-bridge-dispatch-ids.txt(Moving initializr to new JS port #4795, Switch initializr JavaScript build to local ParparVM target #5200) is a sidecar mapping for the opt-in-Dparparvm.js.manglesigsmangler — a debugging aid for reading mangled dispatch ids. Nothing reads the file at runtime.Both are now written only under
-Dparparvm.js.diagnostics=1(or=true).JavascriptTargetIntegrationTestsets it, so the test that inspects them still does.5. Maven descriptors no longer leak into the web root
The translator copies every non-class file out of the app jar, which included
META-INF/maven/**/pom.xml,pom.propertiesandMETA-INF/MANIFEST.MF— publishing the app's group/artifact/version and its build metadata at a fetchable URL.The filter is path-aware: it only skips these names when they sit under a
META-INFdirectory, so an app resource that happens to be calledpom.xmlis still copied.The output zip no longer contains a top-level
<MainClass>-js/wrapper directory. Entries sit at the root, sounzip -d /var/www/appproduces a working web root instead of one directory containing it.This is the point of the PR — the previous layout forced every consumer to unwrap the bundle before serving it — but it will break deployment tooling that expects the wrapper, e.g.
unzip … && cp -R <MainClass>-js/. dest/. The fix is to drop the wrapper hop. Flagging it for the release notes.Also: force the jar to be recreated
Ant's
<jar>treats an existing archive as up to date when it is newer than every input file. The include/exclude list lives in the POM, not in the inputs, so on an incremental build a change to it is silently ignored and a stale jar ships. This cost a full debugging cycle: the exclusions above appeared to do nothing until I noticed the inner jar was hours old. The task deletes the jar first now.Verification
🤖 Generated with Claude Code