Skip to content

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

@derrickbeining

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions