← All decisions

Declare javascript as gleam.toml's default target, discovered as a real gleam publish blocker

toolchainpackaginghex

Context

Attempting the actual `gleam publish` step of decision 0065's Stage 3 (glemy had never declared a top-level `target` in gleam.toml, silently defaulting to Gleam's own tool-wide default, erlang) failed with 'Cannot publish empty modules: glemy/io'. `gleam publish` builds and validates the package against gleam.toml's default target before uploading; io.gleam is entirely `@target(javascript)`-gated by design (browser keyboard/mouse polling has no Erlang/BEAM equivalent), so from an erlang-target build it has zero public definitions at all -- a real Hex rule (not specific to this project) refusing to publish any module with no public content, confirmed against gleam-lang/gleam issue #4093.

Options considered

Add a dummy erlang-visible public item to io.gleam just to satisfy the empty-module check — rejected
Would be exactly the kind of speculative, non-functional stub this project's own standing rules (ARCHITECTURE.md's FFI-minimality rule, 'What NOT to do') exist to prevent -- adding fake public surface area to work around a packaging check, not because a real capability exists there.
Restructure io.gleam so it's not entirely target-gated (e.g. split a genuinely target-agnostic piece out) — rejected
There is no genuinely target-agnostic piece of io.gleam to split out -- every one of its three functions (is_key_down, mouse_position, is_mouse_button_down) is real browser-input polling with no meaningful Erlang/BEAM behavior. Splitting would be structure invented to satisfy a packaging tool, not a real domain boundary.
Leave the default target as erlang (unset) and never resolve this — rejected
Blocks the explicitly requested Stage 3 (a real Hex release) entirely -- not a viable option given the user's own direct instruction to publish.

Decision

Added `target = "javascript"` to gleam.toml's top level. This only changes which target ambiguous CLI invocations (a bare `gleam test`/`gleam run`, and `gleam publish`'s own validation build) default to when no `--target` flag is given -- it does not restrict or change what actually compiles: `gleam test --target erlang` and `gleam test --target javascript` both still pass in full (170 / 190) after this change, confirmed directly, not assumed. Chosen as the honest default given the package's own actual shape: `io.gleam` (and much of Shell) is javascript-only by design, and the browser demos are this package's primary real-world consumption path -- erlang support is real, tested, and valuable, but was never a deliberate default to begin with, only Gleam's own unexamined tool-wide fallback.

Verification

gleam build (default target, now javascript) succeeds cleanly. gleam build --target erlang and gleam test --target erlang both still pass in full (170 tests), and gleam test --target javascript still passes in full (190 tests) -- confirming the declared default target change has zero effect on either target's actual compiled/tested behavior. gleam publish's own build validation (run locally against a real Hex API key, then reverted from the working tree before this decision was written up) confirmed the empty-module error for glemy/io no longer occurs once this default is set.

Consequences

Any future genuinely-erlang-only module would need the same scrutiny this decision gives io.gleam -- a module with real content on only one target is fine and expected in this codebase's Shell layer, but must have at least one item visible on gleam.toml's *declared default* target, or `gleam publish` will refuse it again for the same reason. No consumer-facing behavior changes: a consumer explicitly building for erlang (`gleam build --target erlang` in their own project) is unaffected by this default.

References