Skip to content

Commit 078b313

Browse files
committed
Add :define option, quickbeam-types npm package, fix script init deadlock
:define option — inject globals into JS runtime before script runs, using the native beam_to_js converter (no JSON roundtrip). Values are set via a new define_global NIF that calls JS_SetPropertyStr on the QuickJS global object. Script loading moved to handle_continue — scripts that use Beam.callSync (fs, path, process.env, etc.) no longer deadlock during GenServer init. The start_link caller waits for a :script_loaded message, preserving synchronous error reporting for script failures. Bundler fix: pass import specifiers (not absolute paths) as filenames to OXC so its topo-sort correctly orders dependencies before entry code. npm/quickbeam-types — types package for the Beam API globals, with @types/node as peer dependency for Node API coverage. Example updated to use `/// <reference types="quickbeam-types" />` and real imports.
1 parent 1403a37 commit 078b313

17 files changed

Lines changed: 305 additions & 73 deletions

File tree

examples/static-site/README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ Open `_site/index.html` in a browser.
3434

3535
## How it works
3636

37-
**`priv/ts/build.ts`** — TypeScript with a real npm import:
37+
**`priv/ts/build.ts`** — TypeScript with real npm imports and Node APIs:
3838

3939
```typescript
4040
import { marked } from "marked"
41+
import fs from "node:fs"
42+
import path from "node:path"
4143

4244
const files = fs.readdirSync(contentDir).filter(f => f.endsWith(".md"))
4345
for (const file of files) {
4446
const html = marked.parse(body)
4547
fs.writeFileSync(path.join(outputDir, `${slug}.html`), page)
48+
console.log(` ${slug}.html → ${meta.title}`)
4649
}
4750
```
4851

@@ -52,13 +55,8 @@ for (const file of files) {
5255
{:ok, rt} = QuickBEAM.start(
5356
script: "priv/ts/build.ts",
5457
apis: [:browser, :node],
55-
handlers: %{
56-
"log" => fn [%{"slug" => slug, "title" => title}] ->
57-
IO.puts(" #{slug}.html → #{title}")
58-
end
59-
}
58+
define: %{"contentDir" => "priv/content", "outputDir" => "_site"}
6059
)
61-
QuickBEAM.send_message(rt, %{contentDir: "priv/content", outputDir: "_site"})
6260
```
6361

6462
## What replaces what

examples/static-site/build.exs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
content_dir = Path.join(__DIR__, "priv/content")
22
output_dir = Path.join(__DIR__, "_site")
33

4+
script = Path.join(__DIR__, "priv/ts/build.ts") |> Path.expand()
5+
{:ok, code} = QuickBEAM.JS.Bundler.bundle_file(script)
6+
47
{:ok, rt} =
58
QuickBEAM.start(
6-
script: Path.join(__DIR__, "priv/ts/build.ts"),
79
apis: [:browser, :node],
8-
handlers: %{
9-
"log" => fn [%{"slug" => slug, "title" => title}] ->
10-
IO.puts(" #{slug}.html → #{title}")
11-
end
12-
}
10+
define: %{"contentDir" => content_dir, "outputDir" => output_dir}
1311
)
1412

15-
QuickBEAM.send_message(rt, %{contentDir: content_dir, outputDir: output_dir})
16-
Process.sleep(1000)
13+
{:ok, _} = QuickBEAM.eval(rt, code)

examples/static-site/package-lock.json

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/static-site/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@
22
"private": true,
33
"dependencies": {
44
"marked": "^15.0.0"
5+
},
6+
"devDependencies": {
7+
"quickbeam-types": "file:../../npm/quickbeam-types",
8+
"@types/node": "^22.0.0"
59
}
610
}

examples/static-site/priv/ts/build.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1+
/// <reference types="quickbeam-types" />
12
import { marked } from "marked"
3+
import fs from "node:fs"
4+
import path from "node:path"
25

3-
const { fs, path } = globalThis as any
4-
5-
declare const Beam: {
6-
call(name: string, data?: unknown): void
7-
callSync(name: string, data?: unknown): any
8-
onMessage(handler: (msg: any) => void): void
9-
}
6+
declare const contentDir: string
7+
declare const outputDir: string
108

119
interface FrontMatter {
1210
title: string
@@ -84,30 +82,26 @@ function renderIndex(posts: { slug: string; title: string; date: string }[]): st
8482
</html>`
8583
}
8684

87-
Beam.onMessage((config: { contentDir: string; outputDir: string }) => {
88-
const { contentDir, outputDir } = config
89-
90-
fs.mkdirSync(outputDir, { recursive: true })
85+
fs.mkdirSync(outputDir, { recursive: true })
9186

92-
const files = fs.readdirSync(contentDir).filter((f: string) => f.endsWith(".md"))
93-
const posts: { slug: string; title: string; date: string }[] = []
87+
const files = fs.readdirSync(contentDir).filter((f: string) => f.endsWith(".md"))
88+
const posts: { slug: string; title: string; date: string }[] = []
9489

95-
for (const file of files) {
96-
const source = fs.readFileSync(path.join(contentDir, file), "utf-8")
97-
const { meta, body } = parseFrontMatter(source)
90+
for (const file of files) {
91+
const source = fs.readFileSync(path.join(contentDir, file), "utf-8")
92+
const { meta, body } = parseFrontMatter(source)
9893

99-
if (meta.draft === "true" || meta.draft === true) continue
94+
if (String(meta.draft) === "true") continue
10095

101-
const slug = path.basename(file, ".md")
102-
const html = marked.parse(body) as string
103-
const page = renderPage(meta.title, html)
96+
const slug = path.basename(file, ".md")
97+
const html = marked.parse(body) as string
98+
const page = renderPage(meta.title, html)
10499

105-
fs.writeFileSync(path.join(outputDir, `${slug}.html`), page)
106-
posts.push({ slug, title: meta.title, date: meta.date || "undated" })
100+
fs.writeFileSync(path.join(outputDir, `${slug}.html`), page)
101+
posts.push({ slug, title: meta.title, date: meta.date || "undated" })
107102

108-
Beam.call("log", { slug, title: meta.title })
109-
}
103+
console.log(` ${slug}.html → ${meta.title}`)
104+
}
110105

111-
fs.writeFileSync(path.join(outputDir, "index.html"), renderIndex(posts))
112-
Beam.call("log", { slug: "index", title: `Index (${posts.length} posts)` })
113-
})
106+
fs.writeFileSync(path.join(outputDir, "index.html"), renderIndex(posts))
107+
console.log(` index.html → Index (${posts.length} posts)`)

examples/static-site/test/static_site_test.exs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@ defmodule StaticSiteTest do
1515
QuickBEAM.start(
1616
script: Path.join(__DIR__, "../priv/ts/build.ts") |> Path.expand(),
1717
apis: [:browser, :node],
18-
handlers: %{
19-
"log" => fn _ -> :ok end
20-
}
18+
define: %{"contentDir" => @content_dir, "outputDir" => output_dir}
2119
)
2220

23-
QuickBEAM.send_message(rt, %{contentDir: @content_dir, outputDir: output_dir})
24-
Process.sleep(2000)
21+
Process.sleep(500)
2522

2623
assert File.exists?(Path.join(output_dir, "index.html"))
2724
assert File.exists?(Path.join(output_dir, "hello-world.html"))
@@ -37,5 +34,7 @@ defmodule StaticSiteTest do
3734
assert index =~ "hello-world.html"
3835
assert index =~ "beam-vs-node.html"
3936
refute index =~ "draft-post"
37+
38+
QuickBEAM.stop(rt)
4039
end
4140
end

examples/static-site/tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"target": "ES2022",
5+
"module": "ES2022",
6+
"moduleResolution": "bundler",
7+
"noEmit": true
8+
},
9+
"include": ["priv/ts/**/*.ts"]
10+
}

lib/quickbeam.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ defmodule QuickBEAM do
7474
* `:node` — Node.js compat (process, path, fs, os)
7575
* `[:browser, :node]` — both
7676
* `false` — bare QuickJS engine, no polyfills
77+
* `:define` — `%{String.t() => term()}` of globals to inject before the script runs.
78+
Values are JSON-encoded. Useful for passing config without `Beam.callSync`.
79+
80+
QuickBEAM.start(script: "build.ts", define: %{"outputDir" => "/tmp/site"})
81+
7782
* `:memory_limit` — maximum JS heap in bytes (default: 256 MB)
7883
* `:max_stack_size` — maximum JS call stack in bytes (default: 1 MB)
7984
"""

lib/quickbeam/js/bundler.ex

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,22 @@ defmodule QuickBEAM.JS.Bundler do
3737
end
3838

3939
defp collect_modules(entry_path, node_modules) do
40-
case do_collect(entry_path, node_modules, [], MapSet.new()) do
40+
basename = Path.basename(entry_path)
41+
42+
case do_collect(entry_path, basename, node_modules, [], MapSet.new()) do
4143
{:ok, files, _seen} -> {:ok, Enum.reverse(files)}
4244
{:error, _} = error -> error
4345
end
4446
end
4547

46-
defp do_collect(abs_path, node_modules, files, seen) do
48+
defp do_collect(abs_path, label, node_modules, files, seen) do
4749
if MapSet.member?(seen, abs_path) do
4850
{:ok, files, seen}
4951
else
5052
case File.read(abs_path) do
5153
{:ok, source} ->
5254
seen = MapSet.put(seen, abs_path)
53-
files = [{abs_path, source} | files]
55+
files = [{label, source} | files]
5456

5557
case extract_imports(source, abs_path) do
5658
{:ok, specifiers} ->
@@ -92,7 +94,9 @@ defmodule QuickBEAM.JS.Bundler do
9294
collect_imports(rest, importer, node_modules, files, seen)
9395

9496
{:ok, resolved_path} ->
95-
case do_collect(resolved_path, node_modules, files, seen) do
97+
label = if relative?(specifier), do: Path.basename(resolved_path), else: specifier
98+
99+
case do_collect(resolved_path, label, node_modules, files, seen) do
96100
{:ok, files, seen} ->
97101
collect_imports(rest, importer, node_modules, files, seen)
98102

lib/quickbeam/native.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ defmodule QuickBEAM.Native do
5353
resolve_call_term: [],
5454
reject_call_term: [],
5555
send_message: [],
56+
define_global: [],
5657
memory_usage: [],
5758
dom_find: [],
5859
dom_find_all: [],

0 commit comments

Comments
 (0)