Repository navigation
add Elm language support #1039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add Elm language support #1039
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # Elm | ||
|
|
||
| [Elm](https://elm-lang.org/) is a delightful language for reliable web applications. | ||
| It is a purely functional language that compiles to JavaScript. | ||
|
|
||
| LiveCodes compiles Elm in the browser using | ||
| [@live-codes/elm-wasm](https://www.npmjs.com/package/@live-codes/elm-wasm), | ||
| a WebAssembly build of the Elm compiler. | ||
|
|
||
| ## Basic Demo | ||
|
|
||
| import LiveCodes from '../../src/components/LiveCodes.tsx'; | ||
|
|
||
| export const config = { | ||
| activeEditor: 'script', | ||
| script: { | ||
| language: 'elm', | ||
| content: `module Main exposing (main)\n | ||
| import Browser | ||
| import Html exposing (Html, button, div, h1, text) | ||
| import Html.Events exposing (onClick)\n\n | ||
| type alias Model = | ||
| Int\n\n | ||
| type Msg | ||
| = Increment\n\n | ||
| update : Msg -> Model -> Model | ||
| update msg model = | ||
| case msg of | ||
| Increment -> | ||
| model + 1\n\n | ||
| view : Model -> Html Msg | ||
| view model = | ||
| div [] | ||
| [ h1 [] [ text ("You clicked " ++ String.fromInt model ++ " times.") ] | ||
| , button [ onClick Increment ] [ text "Click me" ] | ||
| ]\n\n | ||
| main : Program () Model Msg | ||
| main = | ||
| Browser.sandbox { init = 0, update = update, view = view } | ||
| `, | ||
| }, | ||
| mode: 'simple', | ||
| editor: 'auto', | ||
| }; | ||
|
|
||
| <LiveCodes config={config}></LiveCodes> | ||
|
|
||
| See below for more examples. | ||
|
|
||
| ## Usage | ||
|
|
||
| Write Elm code in the script editor. The module must expose `main` (e.g. | ||
| `module Main exposing (main)`), as required by the Elm compiler. | ||
|
|
||
| The code is compiled to JavaScript, then the compiled program is initialized in the result page | ||
| by a `Elm.<ModuleName>.init({ node })` call into a `div` appended to the page body. | ||
| If you wish to use a specific DOM element use `"livecodes-app"` as the element `id`. | ||
| This works for the common program types (`Browser.sandbox`, `Browser.element`, `Browser.document` and `Browser.application`). | ||
|
|
||
| Imports of modules from Elm packages are detected automatically. Packages the compiler ships with | ||
| (a set of `elm/*` packages, provided as precompiled artifacts) are enabled without downloading | ||
| anything; any other package is downloaded from jsDelivr and installed into the compiler's virtual | ||
| file system. | ||
|
|
||
| The generated JavaScript can be inspected in the **Compiled code** tool pane. | ||
|
|
||
| ## Language Info | ||
|
|
||
| ### Name | ||
|
|
||
| `elm` | ||
|
|
||
| ### Aliases / Extensions | ||
|
|
||
| `elm` | ||
|
|
||
| ### Editor | ||
|
|
||
| `script` | ||
|
|
||
| ## Compiler | ||
|
|
||
| [@live-codes/elm-wasm](https://www.npmjs.com/package/@live-codes/elm-wasm), a WebAssembly build | ||
| of the [Elm compiler](https://github.com/elm/compiler). | ||
|
|
||
| ### Version | ||
|
|
||
| Elm 0.19.1 (`@live-codes/elm-wasm` 0.2.1) | ||
|
|
||
| ## Code Formatting | ||
|
|
||
| Not supported. | ||
|
|
||
| ## Limitations | ||
|
|
||
| - The first run downloads the compiler (~10.6 MB `ulm.wasm`) in addition to the Elm package | ||
| data. | ||
| - Elm source is compiled as a single module. Other modules are provided by installed packages, | ||
| which are resolved at compile time (jsDelivr by default). | ||
|
|
||
| ## Starter Template | ||
|
|
||
| https://livecodes.io/?template=elm | ||
|
|
||
| ## Links | ||
|
|
||
| - [Elm](https://elm-lang.org/) | ||
| - [Elm guide](https://guide.elm-lang.org/) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -295,6 +295,31 @@ test.describe('Starter Templates from UI', () => { | |||||
| await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); | ||||||
| }); | ||||||
|
|
||||||
| test('elm Starter', async ({ page, getTestUrl }) => { | ||||||
| // the Elm compiler (ulm.wasm, ~10.6 MB) is downloaded on the first run | ||||||
| test.slow(); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The assertion below allows up to 280s, but
Suggested change
|
||||||
|
|
||||||
| await page.goto(getTestUrl()); | ||||||
|
|
||||||
| const { app, getResult, waitForResultUpdate } = await getLoadedApp(page); | ||||||
|
|
||||||
| await app.click('[aria-label="Project"]'); | ||||||
| await app.click('text=New'); | ||||||
| await app.click('text=Elm Starter'); | ||||||
|
|
||||||
| await waitForEditorFocus(app); | ||||||
| await waitForResultUpdate(); | ||||||
|
|
||||||
| // nothing is rendered until the compiler is downloaded and the code has run | ||||||
| await expect(getResult().locator('h1')).toHaveText('Hello, Elm!', { timeout: 280_000 }); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π©Ί Stability & Availability | π‘ Minor | β‘ Quick win π Supported by static analysisπ Script executed: sed -n '285,330p' e2e/specs/starter.spec.ts
sed -n '1,80p' playwright.config.ts
rg -n 'test\.slow\(|setTimeout\(' e2e playwright.config.tsRepository: live-codes/livecodes Length of output: 3761 π Script executed: sed -n '1,35p' e2e/specs/starter.spec.ts
printf '\n--- package manifests ---\n'
rg -n '"`@playwright/test`"|"playwright"' package.json package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock 2>/dev/null | head -40Repository: live-codes/livecodes Length of output: 1123 π Script executed: fixture=$(fd -t f 'test-fixtures' e2e | head -1)
printf '%s\n' "$fixture"
wc -l "$fixture"
cat -n "$fixture"Repository: live-codes/livecodes Length of output: 1270 Set an explicit timeout for the Elm test. π€ Prompt for AI Agents |
||||||
|
|
||||||
| await getResult().click('text=Click me'); | ||||||
| await getResult().click('text=Click me'); | ||||||
| await getResult().click('text=Click me'); | ||||||
|
|
||||||
| await expect(getResult().locator('text=You clicked')).toHaveText('You clicked 3 times.'); | ||||||
| }); | ||||||
|
|
||||||
| test('d3 Starter', async ({ page, getTestUrl, editor }) => { | ||||||
| test.slow(); | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './lang-elm'; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||
| import type { CompilerFunction } from '../../models'; | ||||||
| import { elmWasmBaseUrl } from '../../vendors'; | ||||||
|
|
||||||
| interface ElmCompiler { | ||||||
| compile: (source: string) => Promise<{ js: string; name: string }>; | ||||||
| } | ||||||
|
|
||||||
| declare const self: { | ||||||
| importScripts: (...args: string[]) => void; | ||||||
| ElmWasm: { | ||||||
| createCompiler: (options: { baseUrl: string }) => Promise<ElmCompiler>; | ||||||
| /** renders the structured Elm problems as readable text */ | ||||||
| formatError: (error: unknown) => string; | ||||||
| }; | ||||||
| createElmCompiler: () => CompilerFunction; | ||||||
| }; | ||||||
|
|
||||||
| /** | ||||||
| * Runs the compiled Elm program in the result page. | ||||||
| * Elm's output is a classic script that defines the global `Elm`, so the program is | ||||||
| * initialized here (instead of by the user's code) into a node appended to the page. | ||||||
| */ | ||||||
| const getMountCode = (name: string) => ` | ||||||
| ;(function () { | ||||||
| const node = | ||||||
| document.querySelector('#livecodes-app') || | ||||||
| document.body.appendChild(document.createElement('div')); | ||||||
| try { | ||||||
| Elm[${JSON.stringify(name)}].init({ node }); | ||||||
| } catch (err) { | ||||||
| console.error('Failed to initialize Elm:', err); | ||||||
| } | ||||||
| })(); | ||||||
| `; | ||||||
|
|
||||||
| self.createElmCompiler = (): CompilerFunction => { | ||||||
| let compiler: Promise<ElmCompiler> | undefined; | ||||||
|
|
||||||
| const getCompiler = () => { | ||||||
| if (!compiler) { | ||||||
| compiler = (async () => { | ||||||
| self.importScripts(elmWasmBaseUrl + 'index.umd.js'); | ||||||
| return self.ElmWasm.createCompiler({ baseUrl: elmWasmBaseUrl }); | ||||||
| })(); | ||||||
| // do not keep a failed boot (e.g. a transient network error) | ||||||
| compiler.catch(() => { | ||||||
| compiler = undefined; | ||||||
| }); | ||||||
| } | ||||||
| return compiler; | ||||||
| }; | ||||||
|
|
||||||
| // the compiler (a ~10 MB WASM module plus the Elm package data) is loaded once and kept warm | ||||||
| getCompiler().catch(() => undefined); | ||||||
|
|
||||||
| return async (code) => { | ||||||
| if (!code.trim()) return ''; | ||||||
| try { | ||||||
| const { js, name } = await (await getCompiler()).compile(code); | ||||||
| return `${js.trimEnd()}\n${getMountCode(name)}`; | ||||||
| } catch (err) { | ||||||
| throw new Error(self.ElmWasm.formatError(err)); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||
| } | ||||||
| }; | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { codemirrorLegacy } from '../../editor/codemirror/utils'; | ||
| import type { LanguageSpecs } from '../../models'; | ||
| import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors'; | ||
|
|
||
| export const elm: LanguageSpecs = { | ||
| name: 'elm', | ||
| title: 'Elm', | ||
| compiler: { | ||
| factory: (_config, baseUrl) => { | ||
| (self as any).importScripts(baseUrl + '{{hash:lang-elm-compiler.js}}'); | ||
| return (self as any).createElmCompiler(); | ||
| }, | ||
| }, | ||
| extensions: ['elm'], | ||
| editor: 'script', | ||
| editorSupport: { | ||
| monaco: { languageSupport: monacoLanguagesBaseUrl + 'elm.js' }, | ||
| codemirror: { | ||
| languageSupport: async () => | ||
| codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-elm.js')).elm), | ||
| }, | ||
| }, | ||
| largeDownload: true, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π― Functional Correctness | π‘ Minor | β‘ Quick win
π Supported by static analysis
π Script executed:
Repository: live-codes/livecodes
Length of output: 3170
π Web query:
Elm Browser.document Browser.application init node documentationπ‘ Result:
<search_synthesis>
In Elm, the
Browser.documentandBrowser.applicationfunctions create programs that take over the entire document (the<head>and<body>tags) of the page [1][2]. Because they manage the entire document, they do not accept anodeargument during initialization [1][3][2]. Thenodeargument is specifically associated withBrowser.elementandBrowser.sandbox[4]. These functions create Elm programs that are intended to be embedded within a specific part of an existing HTML page, allowing you to specify a DOM element for the program to take over [5]. When initializing an Elm program that accepts anode(such as one created withBrowser.element), you provide it in the JavaScriptinitcall: var app = Elm.Main.init({ node: document.getElementById('myapp') }); In contrast, sinceBrowser.documentandBrowser.applicationmanage the entire page, they do not take anodeargument [1][2]. You initialize them simply by callingElm.Main.init()(optionally passingflagsif defined) [6][5]. Their configuration is defined entirely within the Elm code, where you provide theinit,view,update, andsubscriptionsfunctions [1][3][2]. Summary of program types: - Browser.sandbox: Does not take a node, but is limited in scope. - Browser.element: Takes anodeargument in JavaScript to embed in a specific DOM element [5]. - Browser.document: Controls the<title>and<body>of the entire document; does not use anode[1][2]. - Browser.application: Manages the entire document and handles URL changes (single-page apps); does not use anode[3][4].</search_synthesis>
<source_evidence>
Citations:
π Script executed:
Repository: live-codes/livecodes
Length of output: 1513
π Web query:
Elm compiler generated JavaScript Browser.document Browser.application init({ node }) behaviorπ‘ Result:
<search_synthesis>
In Elm, the initialization behavior of a compiled program depends on the specific Browser module function used to create the program [1]. When initializing an Elm program via the JavaScript
Elm.Main.init()function, thenodeparameter determines where the Elm application attaches to the DOM [2]. For Browser.element and Browser.sandbox: The node parameter is required [3]. You must provide a reference to an existing DOM element in your HTML file [2]. The Elm runtime will take over this specific element and manage its contents [2][3]. For Browser.document and Browser.application: The node parameter is ignored (or effectively unused) because these program types are designed to manage the entire document [3][4]. - Browser.document: This allows you to control the entire document body and the title of the page [5]. Because it manages the whole page structure, it automatically attaches to the document body rather than a specific sub-element [3]. - Browser.application: This is used for single-page applications and also manages the entire document, including URL navigation [6]. Like Browser.document, it is intended to take over the full page, making the node parameter unnecessary during initialization [3][4]. If you attempt to pass a node to Browser.document or Browser.application, it is typically ignored by the Elm runtime, as the application assumes control of the document body regardless [4]. Conversely, failing to pass a node to Browser.element will result in a runtime error because the Elm runtime expects a target container to render into [7].</search_synthesis>
<source_evidence>
Citations:
π Script executed:
Repository: live-codes/livecodes
Length of output: 1241
Limit node mounting to compatible Elm program types.
The generated initializer always calls
Elm[<ModuleName>].init({ node }).Browser.documentandBrowser.applicationignorenodeand manage the result pageβs document instead. The appendeddivis not their mount target. Restrict this support claim toBrowser.sandboxandBrowser.element, or document the document-level behavior.π€ Prompt for AI Agents