A standard for shipping simple JS with Elm Packages
This repo explores the possibility of an API/standard/spec for Elm packages that provide JS/ports, as well as tooling to make adding that JS/ports to projects seamless and (fully!) type safe.
The idea is being tested out with a Labs implementation in Lamdera.
Contributions to examples
are welcome, particularly if you have a use-case that would be challenging/impossible for the elm-pkg-js proposed approach.
There are a number of common cases where Elm still requires external JS that haven't been codified in an elm/*
or elm-explorations/*
package.
Examples are:
- Browser Media APIs (i.e. Audio)
- Copy paste
- Localstorage
- Websockets
- Internationalization
Until such a time as all these usecases are provided by official packages, most packages that explore this domain require some JS glue to provide the extra integration via ports or webcomponents, and a manual specification of which Elm ports are required, with what types, and how to set them up.
Right now there is no standard for doing this in the community, so it's a manual and ad-hoc process that varies wildly depending on the package.
There are a number of existing libraries for port interop (see Prior Art) but none meet the desired design goals below.
Initially the goal is this works with something non-intrusive i.e. Parcel. The idea is being tested out by a few people experimentally with Lamdera.
If we're happy with the design, it might make sense to share it with Elm community on Discourse as a general proposal for "packages-including-JS".
In that broader context, it would probably be nice to have this system work nicely regardless of JS bundling/minification approaches (webpack, parcel, gulp, browserify, etc).
A "standardised" approach for providing JS alongside Elm packages, that would have:
- Clearly specified interface for providing JS functionality alongside an Elm package
- The location and naming of the JS
- The naming and API/types for the ports
- Ability to find out at "compile time" (not at runtime) issues such as:
- If the port names in the package and the port names in the app mismatch
- If the port names from the package are missing port declarations in the app entirely
- If the type of the ports does not match the API specified by the package
- Easily auto-hooked up into a project, perhaps with a small external tool, in an idempotent way
- Mechanism to provide warnings/overviews when the package author has modified the included JS
We'll refine these goals as we learn more.
- Greater accessibility for packages-with-JS
- Much better comparability of the functionality and APIs of packages-with-JS
- Get us closer to the notion of "good" candidate packages to be possibly included into
elm/*
orelm-explorations/*
with Kernel code in future, or at least clearly showcase the gaps/limitations without - Open up possibility for community-reviewed and vetted JS
- Support existing Elm philosophy on JS integration with improved ergonomics
- Simpler security considerations when evaluating an elm-pkg-js package, in contrast to
npm
.
Check out the Proposal for a work-in-progress description of the specification and tooling behaviour.
See the examples folder for Elm packages that include elm-pkg-js spec
compliant JS as per the Proposal.
elm-pkg-js
would be a specialised tool, i.e similar to elm-json. So "compile time" in this context means "when the user runs elm-pkg-js
", and not some extension to the Elm compiler.
In the Lamdera context, we'd possibly bake this functionality directly into the lamdera
binary, so "compile time" would actually mean compile time in that specific application.
In either case, the design goals would ideally be the same, and work against the same specification.
This initiative isn't about solving "user-added JS" (in contrast to "package-added JS"), though perhaps a design here might lead to a nice encapsulation of user JS for some circumstances too.
When user JS in a project comes into play we also have to consider their entire environment, which is extremely difficult to unify for (i.e. say user project is Rails as a backend and Webpack as build, or some completely different environment we don't anticipate, or they're injecting Elm progressively into an existing JS app with it's own setup, assumptions, build pipeline, etc).
Lamdera has no user written JS so the surface area is much more constrained and manageable.
Bill St. Clair has the elm-port-funnel package, which aligns with some of the problem statement here;
billstclair/elm-port-funnel allows you to use a single outgoing/incoming pair of ports to communicate with the JavaScript for any number of PortFunnel-aware modules, which I'm going to call "funnels".
The downsides to this approach are:
- The user needs to manage integrating the port + package as a "component" (i.e. manually tracking additional 3rd party model state and messages).
- It doesn't work for packages that want to act as frameworks/wrappers (i.e. elm-audio).
SupPort is a small framework for Elm ports. It uses the "one port pair per actor" approach and aims to make it as delightful as possible. There is also a JavaScript component to this as well, which is available here.
This approach appears to be more suited to solving the stated problem, but still has a few gaps:
- Same as
elm-port-funnel
it expects the user will be managing the operational model+msg - Because it's setup with the expectation of user involvement, it doesn't seem as amenable to auto-linking as it could be
- Might serve as a good starting point but needs more research
Was pointed out on Elm Slack, is specifically aimed at request/response modelling with ports but could be good to look over.
Note: unsuitable as it expects functions to be stored in the msg and model types.
This repository doesn't directly address any of the above design points but it does have sample JS integrations that we can validate against whatever candidate solutions we come up with here.
elm-ts-interop
greatly improves the ergonomics of a Typescript <-> Elm boundary via ports, and is a great choice for user-land JS.
It might make less sense for elm-pkg-js
's focus (JS included with Elm packages), as it would mean:
- Forcing the use of Typescript, thus making some transpilation tooling mandatory
- Requiring a dependency on
elm-ts-json
Perhaps authors could apply elm-ts-interop
electively on more complex JS inclusions, but it seems to make most sense that the final output pulled in by elm-pkg-js
is plain JS, and that's also what user's are reviewing in terms of security risk to add to their project.
Definitely a tool to keep an eye on however as things develop.