A general purpose PPX and library for embedding other languages into ReScript, via code generation.
The PPX itself is very very simple - just swap out the embedded language string with a reference to the code generated for that embed. The code generation happens elsewhere. This way embedding languages is flexible and light weight.
This package will eventually ship with a set of utils for making the code generation part easy to set up as well.
Installation
npm i rescript-embed-lang
And then add the PPX to your rescript.json:
"ppx-flags": ["rescript-embed-lang/ppx"]
There, all set!
Why one general PPX
PPXes can be complex and difficult to maintain, and costs at bit of performance. Therefore, this PPX is intended to be extended to support as many use cases around embedding other languages into ReScript as possible. This way, all language embeds built can use one central PPX rather than implementing their own. Maintenance becomes drastically easier, and performance is only hit once if you use several language embeds.
Supported extensions
EdgeQL
You can embed EdgeQL directly as an assignment to a let binding:
// Movies.res let findMovieQuery = %edgeql(` # @name findMovieQuery select Movie { id title } filter .id = <uuid>$movieId `)
Is transformed into:
// Movies.res let findMovieQuery = Movies__edgedb.FindMovieQuery.query
You can also embed it via a module, in case you want easy access to all of the things emitted in the generated code:
// Movies.res module FindMovieQuery = %edgeql(` # @name findMovieQuery select Movie { id title } filter .id = <uuid>$movieId `)
Is transformed into:
// Movies.res module FindMovieQuery = Movies__edgedb.FindMovieQuery
Generic transform
rescript-embed-lang ships with a generic transform, intended to make experimenting with writing new language embeds + generating code for them much easier in user land, without needing you to add a full transform to this PPX. It expects a specific structure (more below) in order to connect your generated code with your ReScript source.
You turn it on by passing -enable-generic-transform in your PPX flags config:
"ppx-flags": [["rescript-embed-lang/ppx", "-enable-generic-transform"]]
It works like this:
// SomeFile.res let myThing = %generated.css(` .button { color: blue; } `)
This will be transformed into:
// SomeFile.res let myThing = SomeFile__css.M1.default
It also works with module references:
// SomeFile.res module MyThing = %generated.css(` .button { color: blue; } `)
Is transformed into:
// SomeFile.res module MyThing = SomeFile__css.M1
Notice that you can put anything to the right of
%generated. The example showscss, but you could use anything else as well. Example:%generated.openapi("...").
The formula for what code to refer to when transforming is be: <filename>__<generated-extension>.M<module-count-for-extension>.default. When using module bindings, the last part .default is omitted.
- We're in
SomeFile.resand usinggenerated.css, so the generated module is expected to be calledSomeFile__css. - Each submodule in your generated file will be called
M+ what number of transform for that extension it is, in the local file. So, the first%generated.cssmodule isM1, the second in that same file isM2, and so on. - Finally, we add a generic
defaulta target value name, just to have something to refer to.
Remember, the actual codegen creating the module we're referring to here from the source
csstext isn't part of this package. This package is just about making it simple to tie together generated things with its source in ReScript.
Deterministic named generation
Generators can opt into one generated file per embed by deriving a stable name. GraphQL generators should use the first-class GraphqlDefinition strategy:
let embed = RescriptEmbedLang.make( ~extensionPattern=Generic("gqlExternalSchema"), ~generatedName=GraphqlDefinition, ~setup=RescriptEmbedLang.defaultSetup, ~generate, ~cliHelpText, )
Value embeds are the primary API:
// Ga4Setup.res let query = %generated.gqlExternalSchema(` query Ga4Properties { ga4Properties { id } } `) await client->run(query, variables)
They also work inline in any expression position:
await client->run( %generated.gqlExternalSchema(` query Ga4Properties { ga4Properties { id } } `), variables, )
The generator emits the stable module Ga4Setup__gqlExternalSchema__Ga4Properties.res. Its generated content is exposed at the module root, so a GraphQL generator can provide variables, response, operation, and default. A value embed expands directly to:
Ga4Setup__gqlExternalSchema__Ga4Properties.default
Use a module embed when callers also want a convenient local name for the generated types and operation:
module Ga4Properties = %generated.gqlExternalSchema(` query Ga4Properties { ga4Properties { id } } `) type variables = Ga4Properties.variables type response = Ga4Properties.response await client->run(Ga4Properties.default, variables)
GraphqlDefinition uses the one named operation in an executable GraphQL document, ignoring any accompanying fragments. If there is no operation, a lone named fragment is accepted. Anonymous operations, multiple operations, and multiple fragments without an operation produce a generator and compile-time error. Schema and other type-system definitions are outside this strategy's scope.
For embeds that carry a name in their source, use NameDirective:
let embed = RescriptEmbedLang.make( ~extensionPattern=Generic("sql"), ~generatedName=NameDirective, ~setup=RescriptEmbedLang.defaultSetup, ~generate, ~cliHelpText, )
NameDirective finds exactly one @name <identifier> in the embedded source. It deliberately does not parse the host language, so generators should reserve @name for the naming directive and avoid including another @name in strings or examples.
Regex is available when neither first-class strategy fits. It supports numbered or named captures and ExactlyOne or First cardinality with ECMAScript regular-expression semantics in both runtimes.
~generatedName=Regex({ pattern: "^-- @name ([_A-Za-z][_0-9A-Za-z]*)", flags: "m", capture: Numbered(1), cardinality: ExactlyOne, })
Generation must run before ReScript compilation. There are no source hashes in the generated API or PPX target; extracted names provide stable generated filenames and module references.
The generator writes a human-readable rescript-embed-lang.json beside its output by default. Point the PPX at that one file:
{
"ppx-flags": [
[
"rescript-embed-lang/ppx",
"-enable-generic-transform",
"-embed-lang-config",
"./src/__generated__/rescript-embed-lang.json"
]
]
}Use --embed-lang-config <path> on the generator only when the config should live somewhere other than <output>/rescript-embed-lang.json. Multiple generators can update different extension entries in the same file.
The PPX loads the config lazily, only when it encounters a named %generated.* embed, and memoizes it for the rest of the process. GraphQL and @name extraction are native; QuickJS is used only for explicit Regex strategies.
Generation is staged before commit, detects case-insensitive and user-module collisions, removes only files recorded in its ownership index, and includes extra emitted artifacts and config updates in the same transaction. The ownership index is removed when the extension has no embeds left; the config remains because it describes how future embeds for that extension compile. Watch runs are serialized and coalesced.
Sequential remains the default, preserving the existing M1, M2, and monolithic-file behavior.
SQL
Embedding for Postgres SQL via pgtyped-rescript.
// Movies.res let findMovieQuery = %sql.one(` /* @name findMovieQuery */ select id, title from movies where id = :id `)
Is transformed into:
// Movies.res let findMovieQuery = Movies__sql.FindMovieQuery.one
Adding more language embeds
Adding more embeds should be straight forward. Reach out if you're interested!