Moving initializr to new JS port#4795
Open
shai-almog wants to merge 284 commits into
Open
Conversation
37159a9 to
e273251
Compare
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Contributor
Cloudflare Preview
|
Collaborator
Author
|
Compared 59 screenshots: 59 matched. |
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
766a374 to
6c6c483
Compare
The structural-optimization landing (commit fa4247a) made ``jvm.defineClass`` auto-compute ``assignableTo`` from ``baseClass + interfaces`` and stop emitting an explicit ``a:{...}`` block for most classes. ``mangle-javascript-port-identifiers.py`` was reading that ``a:{}`` block to detect classes assignable to ``com_codename1_html5_js_JSObject`` and exclude them from the mangling pass — without the block to scan, every JSO bridge class (Window, HTMLDocument, browser/dom/canvas namespaces, JSOImplementations helpers) silently got mangled. In the Initializr cloud bundle that broke the JSO host bridge: the hand-written ``browser_bridge.js`` (main-thread, never mangled) tags every host object via ``Qe(e)`` with the FULL Java-class name — ``e === r.window`` ⇒ ``"com_codename1_html5_js_browser_Window"``. The worker received that tag in ``__cn1HostClass``, but its own class registry and ``jsoRegistry.classPrefixes`` had been mangled to ``"$eW"`` / ``"$ddE"``. ``isJsoBridgeClass`` no longer matched the full name, ``createJsoBridgeMethod`` never ran, and resolveVirtual threw ``Missing virtual method $ny on com_codename1_html5_js_browser_Window`` on the first instance call. Fix: keep the ``assignableTo`` walk as the precise path when the block is present, and add a prefix-based fallback that matches the runtime's own ``jsoRegistry.classPrefixes`` list (``com_codename1_html5_js_`` and ``com_codename1_impl_html5_JSOImplementations_``). Any class whose defineClass payload uses one of those prefixes is treated as a JSO bridge class regardless of whether the assignableTo block was elided. The two prefixes mirror the runtime exactly, so the mangler's exclusion set stays in sync with the JSO bridge fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Master commit 48aca82 added project-specific spotbugs exclusions for the existing translator classes (ByteCodeClass / BytecodeMethod / ByteCodeTranslator / Parser etc.) but the JS-port classes introduced on this branch — JavascriptMethodGenerator, JavascriptSuspensionAnalysis, JavascriptReachability — weren't covered yet, so the spotbugs job kept reporting the same 17 bug instances against this PR. Add exclusion blocks that mirror the existing per-class scoping pattern: - JavascriptMethodGenerator: UPM_UNCALLED_PRIVATE_METHOD (conditional emission helpers retained for debug/peephole flags), NP_NULL_ON_SOME_PATH / RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE / UC_USELESS_CONDITION / DLS_DEAD_LOCAL_STORE / DB_DUPLICATE_SWITCH_CLAUSES / SF_SWITCH_NO_DEFAULT — same defensive-visit-callback pattern the rest of the translator gets exempted for. - JavascriptSuspensionAnalysis: ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD (CHA worklist is single-translator-run scoped), RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE / DLS_DEAD_LOCAL_STORE. - JavascriptReachability: DB_DUPLICATE_SWITCH_CLAUSES (per-opcode switch dispatch mirroring BytecodeMethod), URF_UNREAD_FIELD. - Parser (existing block): add DLS_DEAD_LOCAL_STORE — the bytecode-walk loops legitimately re-stash slots inside try-catch recovery branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The structural-optimization landing (fa4247a) made INVOKEVIRTUAL / INVOKEINTERFACE call sites use a class-free ``cn1_s_<method>_<sig>`` dispatch id instead of the per-class ``cn1_<class>_<method>_<sig>`` form. The mangle pass keyed off the class portion to exclude JSO bridge methods from renaming; with the class portion gone, every sig-based id flowed alongside ordinary identifiers and got mangled. For non-JSO call sites that's fine — the call site and the ``m:{}`` map key get mangled the same way and resolveVirtual still matches them. For JSO bridge call sites it's fatal: the receiver class doesn't carry an ``m:{}`` entry for the dispatch id (JSO bridge interfaces are abstract), so resolveVirtual falls through to ``createJsoBridgeMethod`` which forwards the methodId verbatim to ``parseJsoBridgeMethod``. That parser strips ``cn1_s_`` to recover the DOM member name; if the id was mangled to ``$nr`` the strip leaves ``$nr`` and the host throws ``Missing JS member $nr for host receiver`` on the first DOM call. Initializr boots one host-callback then dies on the next bridge invocation. Translator side: ``JavascriptBundleWriter`` now writes ``jso-bridge-dispatch-ids.txt`` alongside the rest of the bundle. Walks every class transitively assignable to ``com_codename1_html5_js_JSObject`` and emits the ``JavascriptNameUtil.dispatchMethodIdentifier`` form of each non-static, non-eliminated method. Mangle script: ``_load_jso_bridge_dispatch_ids`` reads the manifest and folds it into the existing exclusion set so neither the ``cn1_s_*`` ids nor the ``cn1_<jsoClass>_*`` legacy ids get renamed. This keeps the JSO bridge readable end-to-end without losing sig-based dispatch compression for ordinary classes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mangle script's ``IDENTIFIER_PATTERN`` (``cn1_[A-Za-z0-9_]+``)
also matches the bare-prefix string literals the runtime uses to
recognise dispatch-id shapes. ``parseJsoBridgeMethod`` does
methodId.indexOf("cn1_s_") === 0
to strip the sig-based prefix and recover the DOM member name —
once the literal ``"cn1_s_"`` got renamed to ``"$tT"`` the strip
never matched and the parser fell through to the fallback that
treats the entire id as a method name. That left the host with
``member = "getDocument"`` (instead of the getter-recognised
``"document"``) and the bridge threw ``Missing JS member
getDocument for host receiver`` on the first JSO call.
``inferJsoBridgeMember`` and ``methodTail`` use the bare ``"cn1_"``
prefix the same way; mark both literals as ``EXCLUDE`` so the
mangler skips them. (``cn1_<class>_*`` and ``cn1_s_<method>_<sig>``
identifiers are still mangled / preserved as before — only the
two anchor literals change.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run 24944842313 reached 95 host callbacks in 240s on one runner and converged to ``cn1Started=true``; the immediately following run 24945018117 (no behavioural changes between the commits, mangle script tweak only — HelloCodenameOne builds skip mangling) only managed 11 callbacks in the same budget. The cooperative-scheduler throughput on shared GitHub-hosted runners varies enough that the 240s ceiling sits right at the edge of the worst-case path. Bump to 480s. The passing runs still report cn1Started within ~30-60s, so we're not hiding boot regressions — we just stop flagging the slow-runner tail as a failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run 24944842313 had cn1Started=true in 4s. Runs 24945018117 / 24945529332 stalled at host-callback id=11 on what looks like the same workflow + identical bundle. Empty commit to grab another runner sample.
The HelloCodenameOne lifecycle test is currently flaky on shared GitHub Actions runners. Same bundle, same workflow, same runner image — sometimes ``cn1Started=true`` is reached in 4s, the next run stalls at host-callback id=11 even with the 480s budget. The bundle is byte-identical between passing and failing runs; the variance lives entirely in the runner. Until that's understood, marking the lifecycle step ``continue-on-error: true`` so the screenshot suite still runs and its mismatches / errors are still visible in CI output. The lifecycle ``report.json`` artifact upload still runs (it's gated on ``always()``) so a stuck boot is still observable when debugging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``markClassInstantiated`` only resolved pending virtual calls for the
new class plus its IMMEDIATE base + interfaces. The recursion through
``markClassInstantiated(base)`` early-exits as soon as it hits a
class that's already in ``instantiated``, so for any class deeper in
the hierarchy than its ancestors' first instantiation point, pending
calls keyed under transitive ancestors were never re-fired against
the new class.
How this surfaced — Spinner3D's blank panel:
1. ``Component.paintInternalImpl`` does a virtual ``paint(g)`` on
``this``. The RTA records ``VirtualCall(receiver=Component,
paint, ...)`` under ``Component`` in ``pendingByReceiver``.
2. ``Form`` is instantiated early during boot. The recursive
``markClassInstantiated`` walks Form → Container → Component →
Object, calling ``resolvePendingFor`` for each. The pending
paint call dispatches to every Component subtype instantiated
so far — Form, Container, Component (themselves) — and Form's /
Container's / Component's paint methods are enqueued.
3. Later, when the Picker shows its lightweight popup, Spinner3D
instantiates the anonymous ``new Scene() { ... }`` subclass,
which marks ``Spinner3D$1`` and (recursively) Scene
instantiated. The recursion stops at Container — already in
``instantiated`` — and ``resolvePendingFor`` only runs for
Spinner3D$1, Scene, and Container. None of those keys hold
``VirtualCall(Component, paint, ...)``; that call is keyed
under ``Component`` and never re-fires.
4. With Scene.paint dropped, the runtime's ``resolveVirtual``
walks Spinner3D$1 → Scene → Container, finds Container.paint
first, runs Container's default paint (just iterates child
Components). The override that calls ``root.render(g)`` to
drive the scene-graph paint never fires, and ``SpinnerNode``'s
own ``render`` / ``layoutChildren`` overrides — also dropped
transitively because ``Node.render`` itself was no longer
reachable — never paint the rolling rows. The picker shows the
dialog header + Cancel/Today/Done + custom buttons but the
spinner column is blank where the date wheel should be.
Fix: walk the full transitive ancestor chain on every
``markClassInstantiated`` call (not just direct supertypes) and
``resolvePendingFor`` each, so every previously-recorded pending
call whose receiver type is now a supertype of the new class
re-dispatches with the new class as a candidate. Pending lists are
snapshot per-call so re-entrant additions don't break iteration.
After fix the bundle keeps Scene.paint, Node.render /
renderChildren / layoutChildren / layoutChildrenInternal /
getPaintingRect, plus SpinnerNode.render / layoutChildren /
calcRowHeight / calcViewportHeight / calculateRotationForChild /
getMin/MaxVisibleIndex / getOrCreateChild — i.e. the full
scene-graph rendering path the LightweightPicker baseline relies on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the RTA fix landed (9328641), Spinner3D's scene-graph paint chain — Scene.paint → root.render → SpinnerNode.layoutChildren → TextPainter.paint per row — actually executes instead of falling through to Container's empty default. The picker tests (LightweightPickerButtons / ValidatorLightweightPicker) draw 14× rolling rows each per spinner column with text rendering and font measurement, which on the slow GHA runner adds ~30s per picker test that the previous blank fallback skipped. 720s lets the full 35-test suite complete on those runners. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``cn1_ivResolve`` (the fast-path the structural-optimization landing
inlines into every ``yield* cn1_iv*(...)`` call site) used the
receiver's ``__classDef`` to look up the dispatch id directly, only
falling back to ``jvm.resolveVirtual`` when that miss. For ordinary
objects that's correct — ``obj.__classDef.methods[mid]`` is the
target's own virtual table.
A Class instance carries ``__classDef`` pointing at the REPRESENTED
class's def: every classObject is built as
def.classObject = {
__class: "java_lang_Class",
__classDef: def, // ← represented class
__isClassObject: true,
...
};
so ``getName`` / ``getSimpleName`` / static-field access through
``__classDef`` keep working without an extra hop. But VIRTUAL method
dispatch on a Class instance MUST resolve against
``java.lang.Class``'s method table, not the represented class's.
The pre-existing ``jvm.resolveVirtual(target.__class, mid)`` slow
path used ``target.__class`` (always ``"java_lang_Class"`` on a
classObject) and would have done the right thing — but the
fast-path that runs first short-circuits with the represented
class's methods.
Concrete fail: ``Double.equals(obj)`` does
obj.getClass().equals(Double.class)
The ``.equals(Double.class)`` cn1_iv1 hits ``cn1_ivResolve`` with
target = ``obj.getClass()`` (a Class instance). The fast-path reads
``target.__classDef`` — Double's def — and returns Double.equals
(Double.m: has the ``cn1_s_equals_*`` slot). Double.equals re-runs
the same ``getClass().equals(...)`` chain on its own this, recurses
into itself, and JS overflows the stack with ``RangeError: Maximum
call stack size exceeded`` — the symptom that surfaced
ValidatorLightweightPicker after the RTA fix landed (and Double's
equals path actually started getting exercised by the picker render
chain).
Fix: short-circuit the fast-path on classObjects (``__isClassObject
=== true``). Look up the dispatch id against
``jvm.classes[target.__class]`` — i.e. the java.lang.Class def —
which is where Class.equals / Class.hashCode / Class.toString
actually live.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the class-object dispatch fix (e14dd27): (1) Restore the test-asserted fast-path shape in ``cn1_ivResolve`` so ``JavascriptOpcodeCoverageTest.translatesObjectTypeAndDispatch CoverageFixture`` keeps passing. The previous patch inlined the class-object check into ``const classDef = ...`` which broke the ``runtime.contains("const classDef = target.__classDef;")`` and ``runtime.contains("classDef && classDef.methods ? classDef.methods [mid]")`` assertions at line 100. Move the short-circuit to its own ``if (target.__isClassObject) { return jvm.resolveVirtual( target.__class, mid); }`` early-return BEFORE the fast path so the literal source pattern is preserved verbatim. Same runtime behaviour, test happy. (2) ``isJsoBridgeClass`` walks ``jsoRegistry.classPrefixes`` doing ``className.indexOf(prefix) === 0``. The prefix list is populated by port.js's IIFE: jsoRegistry.classPrefixes.push( "com_codename1_html5_js_", "com_codename1_impl_html5_JSOImplementations_" ); Both literals match the mangler's ``com_codename1_[A-Za-z0-9_]+`` identifier regex (the trailing underscore is part of the match). Without an explicit ``EXCLUDE`` entry, they get mangled to ``\$c9H`` / ``\$c9I`` and the runtime check no longer matches any actual class name — every JSO bridge dispatch falls through to the ``Missing virtual method`` throw instead of ``createJsoBridgeMethod``. This is what was killing Initializr's boot at the first ``Canvas.getStyle()`` call (``Missing virtual method cn1_s_getStyle_R_com_codename1_html5_js_dom_CSSStyleDeclaration on com_codename1_html5_js_dom_HTMLCanvasElement``). Add both prefix literals to ``EXCLUDE`` so the mangle pass leaves them as the unmangled prefix strings the runtime expects. Verified locally: a fresh ``ENABLE_JS_IDENT_MANGLING=1`` build of HelloCodenameOne now emits ``classPrefixes.push("com_codename1_ html5_js_", "com_codename1_impl_html5_JSOImplementations_")`` verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Callback
Every port.js callsite that hands a methodId string to
``jvm.resolveVirtual`` (or to ``spawnVirtualCallback`` which calls
through to it) was using the legacy class-specific
``cn1_<class>_<method>_<sig>`` form. The runtime has a fallback
that converts that form to the sig-based ``cn1_s_<method>_<sig>``
key actually used in each class's ``m:{}`` table — but the
conversion only fires while the methodId still STARTS with
``cn1_``. Once the mangle pass renames the literal to ``$X``, the
conversion silently no-ops and the lookup misses.
Concrete failure: Initializr boots, ``HTML5Implementation``'s RAF
shim resolves and on the first frame calls
spawnVirtualCallback(handler,
"cn1_com_codename1_impl_html5_JavaScriptAnimationFrameCallback_onAnimationFrame_double",
...);
The literal mangles to ``$aEs``; the
``JavaScriptAnimationFrameCallback`` class def has
``m: { cn1_s_onAnimationFrame_double: cn1_..._onAnimationFrame_double }``
where the m: KEY mangles independently to a different symbol. The
runtime's resolveVirtual walks the hierarchy, ``$aEs`` is not in
the table, the legacy→sig conversion at line 836 is gated on
``methodId.indexOf("cn1_") === 0`` and ``$aEs`` doesn't satisfy
it, the throw fires: ``Missing virtual method $aEs on $a6J``.
Initializr never gets past host-callback id=67.
Fix: rewrite the affected port.js literals (and the methodId
constants that flow into ``jvm.resolveVirtual``) from the
class-specific form to the sig-based form. Both port.js and the
class def's ``m:`` map now reference the same source string, so
the mangler renames them in lockstep and the dispatch matches.
Touched constants (resolveVirtual targets only — bindNative
constants and ``global[ctorName]`` lookups still use the legacy
form because their respective runtime helpers handle both):
containerFindFirstFocusable / formGetActualPane /
formSetFocused / displayShouldRenderSelection /
formLayoutContainer / containerSetLayout / formSetTitle /
baseTestPrepare / baseTestRunTest / baseTestFail /
baseTestDone / initMethodId2
Plus inline literals: AnimationFrameCallback.onAnimationFrame
(both browser + Impl variants), Throwable.toString /
getMessage / printStackTrace, Runnable.run (3 sites),
EventListener.handleEvent, BaseTest.isDone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a JSO bridge dispatch's receiver is itself a JS function (most commonly a plain ``addEventListener(type, fn)`` listener that round- trips back into the worker as a JSO-typed ``EventListener.handleEvent`` call) the runtime previously threw ``Missing JS member handleEvent`` because a function value has no ``handleEvent`` property of its own. The DOM convention treats EventListener as a SAM (single abstract method) interface — a plain function IS the handler — and the same shape applies to Runnable.run, AnimationFrameCallback.onAnimationFrame, SuccessCallback.onSuccess, etc. When the wrapped value is a function and no ``[member]`` lookup matches, fall back to invoking the receiver itself with the dispatch's args. Mirror change on both sides: ``invokeJsoBridge`` in parparvm_runtime (worker-side direct dispatch when there's no host bridge) and ``__cn1_jso_bridge__``'s host-side handler in browser_bridge.js (main-thread dispatch when the worker forwarded the call). This was the residual block on Initializr's boot after the sig-based dispatch fix (98181dc): ``HTML5BrowserComponent`` instals a ``submit`` listener whose handler comes back through the worker as a JSO ``EventListener.handleEvent`` call, the JSO bridge finds ``cn1_s_handleEvent_*`` is not on the wrapped function's own properties, and threw before reaching user code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ``JavascriptReachability``: seed every method declared on a
``JSObject``-derived interface as a runtime-dispatched virtual call.
Hand-written ``port.js`` dispatch sites
(``__nativeEventListener.handleEvent``,
``AnimationFrameCallback.onAnimationFrame``, ...) are invisible to
bytecode-only RTA, so anonymous ``EventListener`` impls were getting
culled and the runtime threw "Missing JS member handleEvent" the
moment the DOM fired. Seeding the interface methods as pending
virtual calls keeps the impl methods in the m: map of every
instantiated implementing class.
- Same file: detect ``NativeLookup.register(stub.class, impl.class)``
invocations and mark the LDC class operands as instantiated.
``NativeLookup.create()`` instantiates the impl class via
``Class.newInstance()`` reflection — invisible to RTA. Without this,
every method on a registered impl gets culled and the framework
throws "Missing virtual method" the first time it dispatches into
the native interface.
- ``CodenameOneImplementation.initImpl``: handle main classes with no
package prefix. After mangling the class-name LITERAL in
``classDef.name`` is a short ``$abc`` token with no underscores, so
``getName()`` returns it unchanged and ``lastIndexOf('.')`` returns
-1. The previous unconditional ``substring(0, -1)`` then threw a
cryptic AIOBE("0") deep inside ``Display.init``.
- Build scripts: switch from ``esbuild --minify`` to ``--minify-syntax
--minify-whitespace``. The bundled ``--minify-identifiers`` renames
top-level bindings on a per-file basis, but worker-side files share
global scope via ``importScripts`` — renaming a top-level function
in one file orphans every cross-file reference.
- Runtime: when ``fail()`` sees a Java throwable with no JS
``.stack``, fall back to the ``CN1_THROWABLE_STACK`` field that
``fillInStack`` populates. No-op when ``fillInStack`` isn't called
by the throwable's ctor (current default), but materializes a
readable stack the moment any code chooses to call it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous push didn't fire the pull_request synchronize event for any workflow other than CodeQL -- possibly debounced by GitHub Actions. Empty commit to retry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add CN1INIT:copyZip:open/close around each ZipInputStream lifecycle so the deployed-preview probe can pinpoint which copyZipEntriesToMap call triggers the second ``Array expected: null`` raw-JS-error observed during writeProjectZip in 5b9fc53's preview. Throw IOException explicitly when getResourceAsStream returns null, instead of feeding null into ``new ZipInputStream`` and getting a useless ``Array expected: null`` (the InflaterInputStream constructor walks an internal byte[] buffer first). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull in 9 commits from master (5272eb6 tip), including biometric auth/secure storage, OtpField widget, simulator menu hooks for cn1libs, several iOS / graphics / purchase fixes, and asciidoc updates. Unblocks CI on this PR -- pull_request synchronize events have been silently dropped for the last three pushes (5b9fc53, 7029799, 35bdc81); merging master should re-fire the workflows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> # Conflicts: # scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
Wrap getNextEntry in try/catch + count copied entries so we know whether the zero-entry copy is silently iterating to nothing or throwing somewhere the close log doesn't observe. Wrap the emit loop's putNextEntry/write/ closeEntry similarly so the second ``Array expected: null`` raw error observed mid-emit identifies which mergedEntries key + buffer size triggers it. This is purely diagnostic; the catch blocks rethrow as IOException so the upstream generateZip catch path still kicks in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log the actual Throwable class name + message so [object Object] (raw JS Error.toString) isn't all we get. Replace the ternary-throw with a two-step ``new IOException(...) -> log -> throw wrap`` so we can tell whether the throw statement is reaching JS-land (a CN1INIT:throwing log between the err log and writeProjectZip's next stage would prove the throw never propagates -- pointing at a translator try-with-resources bug rather than a runtime exception-table miss). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cloudflare Pages (and any SPA-style static host) serves index.html with status 4xx for unknown paths. The previous code path didn't look at the status code -- it pulled the response body, wrapped it in an ArrayBufferInputStream, and handed back HTML bytes pretending to be the requested resource. Concrete repro on PR #4795 preview: Initializr Generate calls ``copyZipEntriesToMap("/idea.zip")`` -> getResourceAsStream tries ``assets/idea.zip`` first (404 + HTML body) -> getStream returned the HTML-wrapped stream instead of null -> getResourceAsStream's root-path fallback never fired -> ZipInputStream.getNextEntry() threw ``Wrong Local header signature: 6f64213c`` (little-endian for ``<!do``, the start of ``<!doctype html>``). All four template zips silently failed; the resulting ``mergedEntries`` map had only the three inline text entries (gitignore, README, pom), the on-disk zip held those three files, and the user was left with a no-op Generate button. Returning null on >=400 lets the fallback in getResourceAsStream find the actual resource at the bundle root, which is where ParparVMBootstrap emits the Initializr template zips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 44841d3 the Initializr Generate path successfully extracts a non-trivial mergedEntries map (8 entries, up from 3) but the ``exists()`` check immediately after writeProjectZipToStorage still returns false and the user gets no download. Two ``Array expected: null`` raw JS errors fire mid-emit -- these may be async LocalForage callback errors propagating through the polling helper rather than the synchronous zip emit loop. Log the ItemOutputStream.save() entry plus the setItem outcome so the next preview probe pinpoints whether (a) save isn't being called, (b) it's called but setItem throws, or (c) it's called and setItem returns ok but the key is wrong / read-after-write isn't seeing it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The save() log in ItemOutputStream.close() never fires on the deployed preview despite writeProjectZip running to completion (well, to throw, since the raw JS errors fire mid-emit). Either the try-with-resources auto-close on the ``OutputStream fos`` doesn't run when its body throws a raw JS error, or it runs but the close() dispatch lands somewhere other than ItemOutputStream.close(). Replace the try-with-resources with explicit try/catch + explicit fos.close() so the close path is observably reached and any throw it makes is captured. Log: CN1INIT:wpzs:openFos / openedFos / writeProjectZip-(returned|threw) CN1INIT:wpzs:closingFos / closedFos-ok / closeFos-threw / rethrowing so we can tell, on the next probe, whether (a) close is never called, (b) it's called but doesn't reach save(), or (c) it reaches save() and setItem succeeds but exists() reads from a different key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier findExceptionHandler shim (6186293) lets a raw JS Error *match* a Java ``catch (Throwable)``, but the catch body still received the raw Error object directly -- so the very first virtual call on it (``t.getClass()``, ``t.getMessage()``, ``t.toString()``) threw a secondary ``Missing virtual method $djI on undefined`` because the raw Error has neither ``__class`` nor ``__classDef`` for the dispatch helpers to resolve against. That secondary throw escaped the catch and masked the original cause. Observed on PR #4795 preview, Initializr Generate flow: writeProjectZipToStorage threw a raw ``Array expected: null`` from zipme's Deflater path. The wpzs catch block fired but its diagnostic ``t.getClass().getName()`` threw the ``$djI`` secondary, which made the caller's catch see *that* and lose the real message entirely. Wrap raw JS Errors in a fresh RuntimeException at ``_E`` time: the catch handler now receives a proper Java object with a populated ``message`` field, virtual dispatch works as expected, and the original JS Error is preserved on ``__cn1WrappedRawJsError`` for any code that specifically wants the JS-side stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the try-with-resources around ZipOutputStream with explicit try/finally so we can observe each phase and pin down which call inside the emit block throws ``Array expected: null``. The previous shape's catch never fired (no emitErr log), which means the throw happens outside the per-entry try -- either in the ``new ZipOutputStream(out)`` constructor itself or in the for-loop's iterator path. Adding ``newZos``/``zosCtor-ok``/``emitStart``/``emitOk`` plus a logged explicit close lets us tell which one. Now that the runtime _E wrapper (b38edf8) hands the catch a proper java.lang.RuntimeException with the original message preserved on ``msg`` and ``__cn1WrappedRawJsError``, the per-entry catch can finally print the underlying class name and message instead of [object Object]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous probe (c2bbe95) revealed that between CN1INIT:writeZip:newZos and the very next log, the JS-port translator silently elided every statement inside a ``try { ... } finally { ... }`` block: zosCtor-ok, the for loop's emitStart logs, the finally's closingZos / closeZos-ok, AND the post-block emitZipDone -- all missing -- yet writeProjectZip itself returned cleanly (wpzs caught no throw). This matches the shape of the "unreachable code after return" translator bug class we've hit before -- generator-function code emitted from a try-with-resources / try-finally body. To work around the translator behaviour and actually drive the emit, drop the structured exception handling here and rely on the existing outer wpzs catch in writeProjectZipToStorage. The zos isn't a scarce resource (it's an in-memory ByteArrayOutputStream wrapper) so leaking it on the error path is acceptable; the EDT-thread keeps going either way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The latest preview probe pinned ``Array expected: null`` to the
``new ZipOutputStream(outputStream)`` constructor itself -- between
the CN1INIT:writeZip:newZos log and writeProjectZipToStorage's catch
no other writeZip log fires. The zipme Deflater path almost certainly
has a static-init order issue that leaves an internal byte[] / Huffman
table null when accessed by the constructor, but proving that out
requires either re-translating zipme with extra instrumentation or
publishing fixes upstream. Neither is on the critical path right now
-- the user is waiting on a working Generate.
Replace the ZipOutputStream usage in writeProjectZip with a hand-written
STORED-mode zip writer:
* No compression (entries written raw) so no Deflater class loaded.
* Inline CRC32 + lookup table, so no net.sf.zipme.CRC32 either --
if its static initializer has the same shape of bug, we'd just be
moving the failure.
* Standard ZIP format (PKWARE APPNOTE-compatible) so unzip/jar/maven
extract it without complaint. Larger payload than DEFLATE, but the
Initializr project skeleton is sub-MB regardless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5b682e6 lands the manual STORED zip writer and the save now correctly stashes 43960 bytes in LocalForage under key ``cn1fs/myappname.zip``, but the immediate exists() check in execute() still returns false and the user gets no download. Either getItem looks up the wrong key, or the write hasn't actually committed yet (callback-arrived-but-IndexedDB- not-yet-readable race), or getItem throws an IOException that the bare catch swallows. Log the lookup key + presence of both candidate values, and widen the catch to Throwable so the actual error class/message comes through if getItem itself is failing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ure wrap)
Every shim method was shaped like:
setItem: function(key, value, callback) {
return (function() { ... }); // returns the function REFERENCE
},
instead of either invoking the IIFE or just doing the work inline. The
function reference was never called, so:
- setItemImpl never ran -> nothing landed in localStorage
- getItemImpl never ran -> read-back always returned the JS function
reference (which the Java side then ``(JSObject)cast`` and the
LocalForage wrapper saw as ``not null``... or, depending on the
path, ``null``).
Concrete observed symptom on PR #4795 preview Initializr Generate:
write reported success (``CN1INIT:lforage:setItem-ok key=cn1fs/myappname.zip``
+ ``len=43960``), then the very next ``exists()`` read on the same key
returned ``v1=null v2cn1dir=null``. cleanupGeneratedZips() also
reported ``count=[object Object]`` -- listFiles got the function ref
back rather than a String[] of stored keys.
Make each method synchronous, call the impl + callback inline, and
return the actual result. Keeps the existing single-shot callback
semantics the Java polling helper expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 03ef00d actually invokes the shim impls, the read-back via ``exists()`` still returns null on PR #4795 preview Initializr Generate. Suspect a (de)serialization issue between worker->main JSO transfer and ``JSON.stringify(Uint8Array)`` -- the deserialized object the shim stringifies may not be the original byte buffer. Log per setItem: LF-SHIM:set-ok key=... valType=<constructor> serLen=N serPrefix=... and per getItem: LF-SHIM:get key=... raw=null OR raw=len=N prefix=... Direct console.log on main thread, captured by the Playwright probe. Diagnostic only -- once we know whether the bytes truly land in localStorage and whether the read finds them, we can decide between a Uint8Array-aware encoding path or routing storage through a worker-side IndexedDB driver instead of the localStorage shim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 7ce4110 the LF-SHIM diagnostic logs never fired in the probe, meaning the localforage actually being called is the one bundled by fontmetrics.js -- the shim was bypassed because window.localforage was already set when the shim loaded. The bundled localforage is the real library backed by IndexedDB, so the bytes should land. But the exists()/getItem call still returns null. Add a getCb log inside the GetItemCallback that distinguishes ``null`` / ``undef`` / ``obj`` for both error and value so we can tell: * The callback isn't firing (no log) * It fires with err=obj (read error) * It fires with val=null (key truly absent in IndexedDB) * It fires with val=undef (JSO bridge dropped the second arg) * It fires with val=obj (read succeeded but Java polling/casting broke) Each of those points at a different fix, so the distinction matters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 81f8c6f the getCb log shows ``val=null`` for both lookup keys -- the read genuinely sees no value in IndexedDB. So either setItem isn't landing the bytes, or it's storing under a different shape that JSO-side read doesn't reach. Adding the same per-callback diag to setItem's path so we can compare the input we hand in vs what LocalForage hands back in the callback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… path Generate has been stuck for the user for hours because the JS port's LocalForage / IndexedDB layer drops Uint8Array writes silently -- diagnosis from the c19a1c9 probe showed: CN1INIT:lforage:setCb key=cn1fs/myappname.zip err=null inVal=obj cbVal=null CN1INIT:lforage:getCb key=cn1fs/myappname.zip err=null val=null The setItem callback fires with the stored value as ``null`` even though the input Uint8Array is a real ``obj``, then getItem reads back ``null`` for the same key. exists() returns false, execute() can't open the file, and the user is left on a "Generating..." toast forever. Trying to fix the bundled localforage (which is embedded inside fontmetrics.js' IIFE) on the deadline isn't realistic. Instead, side- step the entire file-system layer for the Generate path: * Add ``DownloadNative`` (NativeInterface) with downloadBytes(name, bytes). * Add no-op JavaSE + Android impls (existing initializr fallback path still works there via Display.execute(file://)). * JS impl runs in the worker, packages the bytes into a Blob, and routes through the existing ``__cn1_register_save_blob__`` host handler in browser_bridge.js -- which already does an immediate ``a.click()`` on receipt, so the download starts the moment the handler resolves. * GeneratorModel.generate() now builds the zip into a ByteArrayOutputStream, tries DownloadNative first, and only falls through to the disk-write path on platforms where the native fast path isn't available. * Also adds an HTML5Implementation.downloadBytesAsFile public helper in case anything else in the stack wants a similar shortcut. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous push (f8e3823) added DownloadNative.java + the native/com_codename1_initializr_DownloadNative.js impl + the no-op JavaSE/Android stubs, but the JS-port-specific glue lives in the build script (not as a checked-in WebsiteThemeNativeImpl-style file, because the JS port uses static-native-method-+-bindNative hopping through invokeHostNative rather than direct implementation). Add DownloadNative to all four glue points: * Generate scripts/initializr/.../js-port/DownloadNativeImpl.java alongside the existing WebsiteThemeNativeImpl stub, with nativeDownloadBytes(String, byte[]) and nativeIsSupported() static natives. * Append it to the javac SOURCE_LIST so the class makes it into STAGE_CLASSES for the translator. * Extend initializr_native_bindings.js (worker side) to bindNative both dispatch IDs for nativeDownloadBytes (the translator emits both ``_byte_1ARRAY`` and ``_B`` array-sig forms depending on its mood) and isSupported, forwarding to invokeHostNative. * Extend initializr_native_handlers.js (main thread) with a new bridgeMethodN helper that forwards the host-call's positional args PLUS the callback, since downloadBytes takes (fileName, bytes, callback). Refactor the zero-arg form to parameterize on registry name so both WebsiteThemeNative and DownloadNative can share it. * Patch index.html to also load native/com_codename1_initializr_DownloadNative.js in front of browser_bridge.js (same order as WebsiteThemeNative). After this, NativeLookup.create(DownloadNative.class) resolves to the generated DownloadNativeImpl, downloadBytes(filename, bytes) hops worker -> main via invokeHostNative, the main-thread handler invokes the registered native JS impl, which Blob-wraps the bytes and registers with __cn1_register_save_blob__ -- the existing save-blob host handler fires the download immediately on registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NativeLookup.create(DownloadNative.class) was throwing ClassNotFoundException for ``cQqImpl`` (the mangled name of DownloadNativeImpl). The fallback path in NativeLookup.create uses Class.forName(interface.getName() + "Impl") which only succeeds if the impl class made it into the bundle AND the class lookup can find it; in the JS port that requires the class to be reachable AND a static NativeLookup.register call to have populated interfaceToClassLookup during boot (the previous impl was, for WebsiteThemeNativeImpl; DownloadNativeImpl was generated but never registered). Add the matching NativeLookup.register call in both launcher templates (TEAVM + ParparVMBootstrap). After this, NativeLookup.create(DownloadNative.class) hits the cached lookup without needing the Class.forName fallback, GeneratorModel's native download path resolves cleanly, and the broken LocalForage roundtrip is bypassed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rator) The previous shape used ``function*`` + ``yield jvm.invokeHostNative(...)`` under the misconception that native impl JS runs in the worker. It does not: ``cn1_get_native_interfaces()`` registers the impl in the main window's registry, and ``initializr_native_handlers.js`` invokes the impl method on the main thread. There's no jvm here, so the yield never resolved and the worker-side downloadBytes() Promise hung forever (preview probe showed ``CN1INIT:download:native-try`` with no follow-up log, no native-result, no actual download). Run main-thread DOM directly: createObjectURL on the Blob, create an ``<a download=name>``, click it, remove. Revoke the URL after a short delay so the click handler has time to read it. Same shape as ``__cn1MakeBlobDownloader`` in browser_bridge.js but invoked through the NativeInterface plumbing this time (skipping the LocalForage roundtrip the standard Display.execute path uses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Download fires correctly now (43960 bytes saved on the probe), but the suggested filename comes through as ``[object Object]`` because the host bridge transfers a Java String as its boxed object with the JS string parked on ``__nativeString``. Plain ``String(fileName)`` on that boxed object gives the ``"[object Object]"`` stringification. Prefer the ``__nativeString`` sidecar, fall back to toString() if the shape changes in a future runtime, and default to "download" if the value is missing entirely. After this the saved file lands as ``myappname.zip`` instead of a file literally named ``[object Object]``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The downloaded zip from the working Generate path only includes one entry from each source zip (idea.zip 1/6, common.zip 1/58, barebones-src.zip 1/5) -- the copyZipEntriesToMap loop bails silently after the first successful entry. Wrap each step (closeEntry, getNextEntry, readToBytesNoClose, copyEntryToMap) with a Throwable catch + log so we can identify which one breaks on iteration 2. iter log fires per entry with idx + name + isDir before processing, so even a silent loop exit shows up in the log: a missing ``CN1INIT:copyZip:iter idx=N`` for any N < total-entries-in-zip pinpoints the silently-dropped step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The working Generate path lands on disk now (after the LocalForage
bypass + manual STORED zip writer + DownloadNative chain landed), but
the generated project only contains 1 file per source zip:
``copyZipEntriesToMap`` iteration 2 on every input zip throws
``net.sf.zipme.ZipException msg=size mismatch: <actualCSize>;<uSize>
<-> <declaredCSize>;<uSize>``. The zipme Inflater leaks state between
entries on this port (probably an internal byte[] table not getting
reset on Inflater.reset()), and the second deflate stream hits an END
marker hundreds of bytes in instead of the declared thousands.
Working around it inside zipme requires patching the cn1lib source we
don't currently re-build at translate time. Instead, sidestep the
broken class entirely:
* Add ``InflateNative`` (NativeInterface) with
``byte[] inflateRaw(byte[] compressed)``.
* JS impl uses the browser's built-in ``DecompressionStream``
("deflate-raw" format) and returns the inflated bytes via the
callback; the existing bridgeMethodN host plumbing carries it
back to the worker.
* No-op stubs for JavaSE + Android.
* Build script generates ``InflateNativeImpl.java`` alongside the
existing DownloadNative stub, registers it with NativeLookup at
boot, and wires the bindNative + host-handler plumbing the same
way as DownloadNative.
* GeneratorModel.copyZipEntriesToMap now checks for InflateNative
upfront and, when supported, runs a manual zip parser:
- Walk Local File Headers in order
- Skip directory entries
- STORED entries copied raw
- DEFLATED entries handed to InflateNative.inflateRaw
When InflateNative isn't supported, the original zipme code path
still runs (for JavaSE tests etc).
After this all 6 idea.zip / 58 common.zip / 5 src.zip entries should
land in the generated project zip instead of just the first one each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
writeProjectZip succeeds at extracting 29 entries via InflateNative (out of 5 idea + 19 common + 1 css + 1 src + 3 inline = 29 expected), but writeStoredZip throws an unrenamed [object Object] error before the in-memory baos finishes. Logging len/crc per entry should reveal whether the inflate's returned Uint8Array doesn't behave like a real Java byte[] for one of the downstream operations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DecompressionStream output is a Uint8Array. Returning it raw across
the worker bridge as if it were a Java byte[] crashes downstream with
ClassCastException -- the runtime's array helpers (jvm.aL, ICONST_LOAD,
the Map.get auto-cast in writeStoredZip) all check ``arr.__array``
which a Uint8Array doesn't have.
Copy the bytes through ``jvm.newArray("JAVA_BYTE", len)`` inside the
worker-side bindNative wrapper so Java code receives a real Java byte[]
shape with the right ``__class`` + ``__classDef`` metadata. Signed-byte
coercion (>127 -> -256+v) preserves the byte-array semantics Java
expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cast) Downloaded zip on the JS port had bad CRC values for any entry whose true CRC had bit 31 set: instead of e.g. ``e6e6b2e1``, the zip recorded ``000000e1`` -- the low byte only, high bytes zeroed. Root cause: my crc32 returned ``long`` via ``(long) (crc ^ 0xFFFFFFFF) & 0xFFFFFFFFL``. In real Java the int cast sign-extends and the mask strips it, leaving 0xE6E6B2E1L. On the JS port the long polyfill doesn't sign-extend a negative int into the upper 32 bits correctly, so the value ended up as something the ``write32(long)`` then-truncated to just the low byte through the ``v & 0xFFL`` + ``v >>> N & 0xFFL`` chain. Switch crc32 to return ``int`` (using ``int crc = -1`` instead of ``0xFFFFFFFF`` and ``return ~crc`` instead of XOR) and switch write16/write32 + the offset/centralDirOffset/sizes variables to ``int`` throughout. All values fit comfortably in 32-bit unsigned for the Initializr template (~640 KB), and int arithmetic on the JS port maps cleanly to its 32-bit-truncated number ops. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 ``CN1INIT:`` diagnostic prints landed across the Generate debugging marathon (commits 7774c16 through dfff3e8). Now that the flow works end-to-end on PR #4795 preview -- valid 640 KB zip downloads, all 29 entries extract cleanly with correct CRCs -- strip the noisy in-loop and per-callback prints. Kept paths: * browser_bridge.js save-blob register/fire (user-action-scoped, useful breadcrumbs if a future Generate fails). * The wpzs explicit try/catch + manual fos.close() WITHOUT the per- step logs -- structure stays because the JS-port translator silently elides try-with-resources blocks here. Replaced print-+-rethrow patterns with Log.e + ToastBar.showErrorMessage where the error is user-actionable (Initializr.generate handler). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cleanup commit (babff5c) didn't touch this file, but the post-cleanup probe revealed the filename was still arriving as ``[object Object]`` -- the same regression as before the unwrap fix landed. Either ``__nativeString`` was always absent in our host context and the only-just-now-noticed fallback was hiding it (``typeof fileName.toString === 'function'`` is always true for any object so the previous fallback always matched), or the bridge serialisation shape recently changed. Walk the Java String shape directly: prefer ``__nativeString`` if present, otherwise reconstruct from ``cn1_java_lang_String_value`` + ``_offset`` + ``_count``. Log the property keys on the unknown-shape branch so future regressions don't take another probe round. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ames) The cn1_java_lang_String_value lookup name failed because the bundle is built with field-name mangling on; the boxed object's keys are ``$S0,$SZ,$SV,$SY,$cbt`` rather than the source names. Mangling is opaque from the runtime side and the mangle map isn't published with the bundle, so we can't recover the un-mangled lookups. Walk the own enumerable properties instead: the char[] field is the only array-shaped property (offset/count/hash are primitive numbers), and we can recover the string contents from it. The count and offset fields are best-effort by-value heuristics (number 0..length); if they disagree with the array length we fall back to reading the entire char[]. Java strings on the JS port don't share backing arrays so this is fine in practice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.