Skip to content

lamdera make --no-wire emits duplicate _Platform_initialize, breaking elm-watch/Elm Land dev builds #97

Description

@derrickbeining

Summary

When using lamdera make --no-wire as a drop-in replacement for elm make in an Elm Land dev server, the generated JavaScript contains two definitions of _Platform_initialize.

This breaks downstream tooling that post-processes Elm compiler output, specifically Elm Land's dev server via vite-plugin-elm-watch / elm-watch.

The important detail is:

  1. Lamdera first generates JS with two _Platform_initialize declarations.
  2. elm-watch then post-processes that generated JS.
  3. elm-watch successfully rewrites the first, standard Elm-shaped _Platform_initialize.
  4. elm-watch does not rewrite the second Lamdera-shaped _Platform_initialize near the end of the file.
  5. Because JavaScript function declarations with the same name share the same scope, the later unchanged Lamdera declaration overrides the elm-watch-patched declaration at runtime.

The visible browser failure is:

TypeError: Cannot read properties of undefined (reading '$')
    at $elm$core$Result$isOk
    at _Platform_initialize

Why elm-watch misses the second initializer

elm-watch is already running after Lamdera output is generated. The issue is not ordering. The issue is that the later Lamdera initializer has a different emitted shape than the normal Elm runtime initializer.

elm-watch's replacement regex expects a standard-looking function body, roughly:

function _Platform_initialize(...)
{
  ...
}

The first _Platform_initialize in Lamdera's output has that standard shape, so elm-watch rewrites it.

But the second _Platform_initialize near the end of the Lamdera output looks like this:

function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder)
  {
    var result = A2(_Json_run, flagDecoder, _Json_wrap(args ? args['flags'] : undefined));

    // @TODO need to figure out how to get this to automatically escape by mode?
    //$elm$core$Result$isOk(result) || _Debug_crash(2 /**/, _Json_errorToString(result.a) /**/);
    $elm$core$Result$isOk(result) || _Debug_crash(2 /**_UNUSED/, _Json_errorToString(result.a) /**/);
    // ...
  }}(this));

This differs from the normal shape in at least two ways:

  • the opening { is indented ( {) rather than starting at the beginning of the next line
  • the function is fused into the closing IIFE tail as }}(this));

So elm-watch leaves this second declaration unchanged.

Why that causes the runtime crash

After elm-watch injection, call sites are rewritten to call _Platform_initialize with elm-watch's expanded signature:

_Platform_initialize(
  impl._impl ? "Browser.application" : "Browser.document",
  false,
  debugMetadata,
  flagDecoder,
  args,
  impl.init,
  impl,
  stepperBuilder
)

That call is correct for elm-watch's patched initializer:

function _Platform_initialize(programType, isDebug, debugMetadata, flagDecoder, args, init, impl, stepperBuilder)

But the later unchanged Lamdera declaration overrides that patched function and still has the standard 6-argument shape:

function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder)

As a result, the runtime calls the wrong function shape:

  • flagDecoder receives elm-watch's first string argument, e.g. "Browser.application"
  • args receives false
  • init, update, and subscriptions are shifted into the wrong positions

Then initialization crashes at the Result.isOk check.

Reproduction

In an Elm Land application using Lamdera as the elm executable:

  1. Configure Elm Land's dev command so that elm resolves to Lamdera, for example with a PATH wrapper where:

    elm make ...

    delegates to:

    lamdera make --no-wire ...
  2. Start the Elm Land dev server:

    elm-land server
  3. Open the app in a browser.

Expected:

The app starts normally, as it does with standard elm make output.

Actual:

The app crashes during initialization with:

TypeError: Cannot read properties of undefined (reading '$')
    at $elm$core$Result$isOk
    at _Platform_initialize

Output-level verification

Compile an Elm Land Browser.application entrypoint with Lamdera:

lamdera make --no-wire .elm-land/src/Main.elm --output=lamdera-output.js

Then inspect the generated JS:

rg "function _Platform_initialize" lamdera-output.js

Observed: the raw Lamdera output contains two definitions:

function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder)
...
function _Platform_initialize(flagDecoder, args, init, update, subscriptions, stepperBuilder)

I also verified this through elm-watch's actual transform:

  • raw Lamdera output had _Platform_initialize definitions at approximately lines 1876 and 146718
  • after elm-watch / vite-plugin-elm-watch injection, the first initializer was rewritten to elm-watch's patched implementation
  • the second Lamdera initializer remained unchanged
  • transformed call sites used elm-watch's expanded initializer signature
  • the later unchanged Lamdera initializer therefore overrides the patched initializer at runtime

Environment

  • Lamdera npm package: lamdera@0.19.1-1.4.0
  • Elm Land: 0.20.1
  • Downstream dev tooling: vite-plugin-elm-watch / elm-watch
  • Command: lamdera make --no-wire .elm-land/src/Main.elm --output=...

Possible fixes

I realize this involves downstream tooling that patches Elm compiler internals, so this may not ultimately be Lamdera's responsibility. I opened this here because the duplicate same-named global runtime declaration is surprising compiler output, and it makes --no-wire output difficult to use as a frontend elm make replacement.

Possible directions:

  • Avoid emitting the duplicate initializer in --no-wire output when it is not needed.
  • Give Lamdera-specific helper runtime functions unique names so they cannot override Elm runtime functions.
  • Otherwise make the emitted JS shape closer to standard elm make output when --no-wire is used as a frontend compiler replacement.
  • If this output shape is intentional, documenting that --no-wire output is not expected to be compatible with tools like elm-watch would also be helpful.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions