Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ extra/node_modules
extra/.cache
.stack-work

# elm-format submodule build artifacts
vendor/elm-format/.stack-work/
vendor/elm-format/**/.stack-work/
vendor/elm-format/src/Main

# tools node_modules
tools/*/node_modules/
tools/**/node_modules/

# test artifacts
test/Test/Export/elm-stuff/
test/Test/Export/*.js
test/Test/Export/*.d.ts

# @TESTS
elm-home

4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "test/scenario-elm-pages-incompatible-wire/elm-pages"]
path = test/scenario-elm-pages-incompatible-wire/elm-pages
url = git@github.com:dillonkearns/elm-pages.git
url = https://github.com/dillonkearns/elm-pages.git
[submodule "vendor/elm-format"]
path = vendor/elm-format
url = git@github.com:lamdera/elm-format.git
url = https://github.com/lamdera/elm-format.git
66 changes: 66 additions & 0 deletions COMMIT_MESSAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
Add --experimental-js-ts-exports flag for JavaScript/TypeScript interop

This feature enables Elm modules to be consumed directly from JavaScript
and TypeScript projects while maintaining Elm's currying semantics.

## Changes

- Add `--experimental-js-ts-exports` CLI flag to `lamdera make`
- Generate ES6 module exports for all top-level functions in modules with main
- Support both curried and uncurried function calls from JavaScript
- Generate TypeScript declaration files (.d.ts) alongside JavaScript output
- Filter internal wire protocol functions (w3_ prefix) from exports

## Implementation Details

- Uses existing Lamdera global flag pattern for consistency
- Reuses JavaScript AST generation infrastructure
- Generates clean module.exports structure for Node.js compatibility
- Preserves Elm's currying by attaching curry property to multi-arg functions

## Testing

- Added comprehensive property-based tests with random Elm module generation
- Tests verify TypeScript declarations compile successfully
- Integration with existing test suite

## Usage

```bash
lamdera make Main.elm --experimental-js-ts-exports --output=output.js
```

This generates:
- output.js with exported functions
- output.d.ts with TypeScript declarations

## Example

Given an Elm module:
```elm
module Main exposing (main)

greet : String -> String -> String
greet firstName lastName =
"Hello, " ++ firstName ++ " " ++ lastName

main = ...
```

JavaScript usage:
```javascript
const { Main } = require('./output.js');

// Direct call (uncurried)
Main.greet("John", "Doe"); // "Hello, John Doe"

// Curried call
Main.greet.curry("John")("Doe"); // "Hello, John Doe"
```

TypeScript gets full type safety:
```typescript
import { Main } from './output';

const greeting: string = Main.greet("John", "Doe");
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this was supposed to be committed?

125 changes: 122 additions & 3 deletions builder/src/Generate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
module Generate
( debug
, dev
, devWithExportFlag
, prod
, repl
, debugWithTypeScript
, devWithTypeScript
, prodWithTypeScript
)
where


import Prelude hiding (cycle, print)
import Control.Concurrent (MVar, forkIO, newEmptyMVar, newMVar, putMVar, readMVar)
import Control.Monad (liftM2)
import Control.Monad (liftM2, sequence)
import qualified Data.ByteString.Builder as B
import Data.Map ((!))
import qualified Data.Map as Map
Expand All @@ -27,6 +31,7 @@ import qualified Elm.ModuleName as ModuleName
import qualified Elm.Package as Pkg
import qualified File
import qualified Generate.JavaScript as JS
import qualified Generate.TypeScript as TS
import qualified Generate.Mode as Mode
import qualified Nitpick.Debug as Nitpick
import qualified Reporting.Exit as Exit
Expand Down Expand Up @@ -63,13 +68,36 @@ debug root details (Build.Artifacts pkg ifaces roots modules) =
return $ JS.generate mode graph mains


debugWithTypeScript :: FilePath -> Details.Details -> Build.Artifacts -> Bool -> Task (B.Builder, B.Builder)
debugWithTypeScript root details artifacts@(Build.Artifacts pkg ifaces roots modules) exportAllFunctions =
do loading <- loadObjects root details modules
types <- loadTypes root ifaces modules
objects <- finalizeObjects loading
let mode = Mode.Dev (Just types)
let graph_ = objectsToGlobalGraph objects
graph <- Task.io $ Lamdera.AppConfig.injectConfig graph_
let mains = if exportAllFunctions
then gatherAllExports pkg objects roots
else gatherMains pkg objects roots
let jsBuilder = JS.generate mode graph mains
interfaces <- collectInterfaces root artifacts
let tsBuilder = TS.generate interfaces mains graph
return (jsBuilder, tsBuilder)


dev :: FilePath -> Details.Details -> Build.Artifacts -> Task B.Builder
dev root details (Build.Artifacts pkg _ roots modules) =
dev root details artifacts =
devWithExportFlag root details artifacts False

devWithExportFlag :: FilePath -> Details.Details -> Build.Artifacts -> Bool -> Task B.Builder
devWithExportFlag root details (Build.Artifacts pkg _ roots modules) exportAllFunctions =
do objects <- finalizeObjects =<< loadObjects root details modules
let mode = Mode.Dev Nothing
let graph_ = objectsToGlobalGraph objects
graph <- Task.io $ Lamdera.AppConfig.injectConfig graph_
let mains = gatherMains pkg objects roots
let mains = if exportAllFunctions
then gatherAllExports pkg objects roots
else gatherMains pkg objects roots
return $ JS.generate mode graph mains


Expand All @@ -95,6 +123,75 @@ repl root details ansi (Build.ReplArtifacts home modules localizer annotations)
return $ JS.generateForRepl ansi localizer graph home name (annotations ! name)


devWithTypeScript :: FilePath -> Details.Details -> Build.Artifacts -> Bool -> Task (B.Builder, B.Builder)
devWithTypeScript root details artifacts@(Build.Artifacts pkg ifaces roots modules) exportAllFunctions =
do objects <- finalizeObjects =<< loadObjects root details modules
let mode = Mode.Dev Nothing
let graph_ = objectsToGlobalGraph objects
graph <- Task.io $ Lamdera.AppConfig.injectConfig graph_
let mains = if exportAllFunctions
then gatherAllExports pkg objects roots
else gatherMains pkg objects roots
let jsBuilder = JS.generate mode graph mains
interfaces <- collectInterfaces root artifacts
let tsBuilder = TS.generate interfaces mains graph
return (jsBuilder, tsBuilder)


prodWithTypeScript :: FilePath -> Details.Details -> Build.Artifacts -> Bool -> Task (B.Builder, B.Builder)
prodWithTypeScript root details artifacts@(Build.Artifacts pkg ifaces roots modules) exportAllFunctions =
do objects <- finalizeObjects =<< loadObjects root details modules
checkForDebugUses objects
let graph_ = objectsToGlobalGraph objects
graph <- Task.io $ Lamdera.AppConfig.injectConfig graph_
longNamesEnabled <- Task.io $ Lamdera.useLongNames
let mode = Mode.Prod (Mode.shortenFieldNames graph)
& Lamdera.alternativeImplementationWhen longNamesEnabled
(Mode.Prod (Mode.legibleFieldNames graph))
let mains = if exportAllFunctions
then gatherAllExports pkg objects roots
else gatherMains pkg objects roots
let jsBuilder = JS.generate mode graph mains
interfaces <- collectInterfaces root artifacts
let tsBuilder = TS.generate interfaces mains graph
return (jsBuilder, tsBuilder)



-- COLLECT INTERFACES


collectInterfaces :: FilePath -> Build.Artifacts -> Task (Map.Map ModuleName.Canonical I.Interface)
collectInterfaces root (Build.Artifacts pkg deps _ modules) = Task.io $ do
let
freshInterfaces = Map.fromList
[ (ModuleName.Canonical pkg name, iface)
| Build.Fresh name iface _ <- modules
]

-- For cached modules, we need to load the interfaces from disk
cachedInterfaces <- fmap Map.fromList $ fmap Maybe.catMaybes $ sequence
[ do -- Try to load the interface from disk
maybeIface <- File.readBinary (Stuff.elmi root name)
case maybeIface of
Just iface -> return $ Just (ModuleName.Canonical pkg name, iface)
Nothing -> return Nothing
| Build.Cached name _ _ <- modules
]

let
localInterfaces = Map.union freshInterfaces cachedInterfaces
foreignInterfaces = Map.mapMaybe depInterfaceToInterface deps

return $ Map.union localInterfaces foreignInterfaces


depInterfaceToInterface :: I.DependencyInterface -> Maybe I.Interface
depInterfaceToInterface dep =
case dep of
I.Public iface -> Just iface
I.Private _ _ _ -> Nothing


-- CHECK FOR DEBUG

Expand All @@ -114,6 +211,11 @@ gatherMains :: Pkg.Name -> Objects -> NE.List Build.Root -> Map.Map ModuleName.C
gatherMains pkg (Objects _ locals) roots =
Map.fromList $ Maybe.mapMaybe (lookupMain pkg locals) (NE.toList roots)

-- Gather all exports (including modules without main) for --export-all-functions
gatherAllExports :: Pkg.Name -> Objects -> NE.List Build.Root -> Map.Map ModuleName.Canonical Opt.Main
gatherAllExports pkg (Objects _ locals) roots =
Map.fromList $ Maybe.mapMaybe (lookupMainOrExports pkg locals) (NE.toList roots)


lookupMain :: Pkg.Name -> Map.Map ModuleName.Raw Opt.LocalGraph -> Build.Root -> Maybe (ModuleName.Canonical, Opt.Main)
lookupMain pkg locals root =
Expand All @@ -125,6 +227,23 @@ lookupMain pkg locals root =
Build.Inside name -> toPair name =<< Map.lookup name locals
Build.Outside name _ g -> toPair name g

-- For --export-all-functions, create a synthetic main that references all exports
lookupMainOrExports :: Pkg.Name -> Map.Map ModuleName.Raw Opt.LocalGraph -> Build.Root -> Maybe (ModuleName.Canonical, Opt.Main)
lookupMainOrExports pkg locals root =
case lookupMain pkg locals root of
Just result -> Just result
Nothing ->
-- If no main, create a synthetic one that marks the module for export
case root of
Build.Inside name ->
case Map.lookup name locals of
Just (Opt.LocalGraph _ nodes _) ->
-- Create a synthetic main that marks this module for export
Just (ModuleName.Canonical pkg name, Opt.Static)
Nothing -> Nothing
Build.Outside name _ (Opt.LocalGraph _ nodes _) ->
Just (ModuleName.Canonical pkg name, Opt.Static)



-- LOADING OBJECTS
Expand Down
Loading