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:
- Lamdera first generates JS with two
_Platform_initialize declarations.
- elm-watch then post-processes that generated JS.
- elm-watch successfully rewrites the first, standard Elm-shaped
_Platform_initialize.
- elm-watch does not rewrite the second Lamdera-shaped
_Platform_initialize near the end of the file.
- 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:
-
Configure Elm Land's dev command so that elm resolves to Lamdera, for example with a PATH wrapper where:
delegates to:
lamdera make --no-wire ...
-
Start the Elm Land dev server:
-
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!
Summary
When using
lamdera make --no-wireas a drop-in replacement forelm makein 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:
_Platform_initializedeclarations._Platform_initialize._Platform_initializenear the end of the file.The visible browser failure is:
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:
The first
_Platform_initializein Lamdera's output has that standard shape, so elm-watch rewrites it.But the second
_Platform_initializenear the end of the Lamdera output looks like this:This differs from the normal shape in at least two ways:
{is indented ({) rather than starting at the beginning of the next line}}(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_initializewith elm-watch's expanded signature:That call is correct for elm-watch's patched initializer:
But the later unchanged Lamdera declaration overrides that patched function and still has the standard 6-argument shape:
As a result, the runtime calls the wrong function shape:
flagDecoderreceives elm-watch's first string argument, e.g."Browser.application"argsreceivesfalseinit,update, andsubscriptionsare shifted into the wrong positionsThen initialization crashes at the
Result.isOkcheck.Reproduction
In an Elm Land application using Lamdera as the
elmexecutable:Configure Elm Land's dev command so that
elmresolves to Lamdera, for example with a PATH wrapper where:delegates to:
Start the Elm Land dev server:
Open the app in a browser.
Expected:
The app starts normally, as it does with standard
elm makeoutput.Actual:
The app crashes during initialization with:
Output-level verification
Compile an Elm Land
Browser.applicationentrypoint with Lamdera:Then inspect the generated JS:
rg "function _Platform_initialize" lamdera-output.jsObserved: the raw Lamdera output contains two definitions:
I also verified this through elm-watch's actual transform:
_Platform_initializedefinitions at approximately lines 1876 and 146718elm-watch/vite-plugin-elm-watchinjection, the first initializer was rewritten to elm-watch's patched implementationEnvironment
lamdera@0.19.1-1.4.00.20.1vite-plugin-elm-watch/elm-watchlamdera 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-wireoutput difficult to use as a frontendelm makereplacement.Possible directions:
--no-wireoutput when it is not needed.elm makeoutput when--no-wireis used as a frontend compiler replacement.--no-wireoutput is not expected to be compatible with tools like elm-watch would also be helpful.Thanks!