Skip to content

Commit 30a596c

Browse files
authored
feat:Add playground (#600)
Motivation: Add a sjsonnet playground, it's single-paged. The html is written by AI, but I attached a spec.md for later improvement. refs: #353 <img width="1916" height="970" alt="image" src="https://github.com/user-attachments/assets/b2f471a9-f012-493d-a1ee-5f23abfbbd1f" />
1 parent b075bcf commit 30a596c

7 files changed

Lines changed: 3335 additions & 29 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Deploy Playground to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
paths:
7+
- 'playground/**'
8+
- 'sjsonnet/src/**' # shared core (parser, evaluator, stdlib) — used by JS build
9+
- 'sjsonnet/src-js/**' # JS-specific code (SjsonnetMain, JsVirtualPath)
10+
- 'build.mill'
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
pages: write
16+
id-token: write
17+
18+
concurrency:
19+
group: "pages"
20+
cancel-in-progress: true
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-22.04
25+
steps:
26+
- uses: actions/checkout@v6
27+
28+
- uses: actions/setup-java@v5
29+
with:
30+
java-version: 17
31+
distribution: 'zulu'
32+
33+
- name: Build Playground Bundle
34+
run: ./mill playground.bundle
35+
36+
- name: Prepare Pages artifact
37+
run: |
38+
mkdir -p _site
39+
cp out/playground/bundle.dest/index.html _site/
40+
41+
- name: Upload Pages artifact
42+
uses: actions/upload-pages-artifact@v3
43+
with:
44+
path: _site
45+
46+
deploy:
47+
needs: build
48+
runs-on: ubuntu-22.04
49+
environment:
50+
name: github-pages
51+
url: ${{ steps.deployment.outputs.page_url }}
52+
steps:
53+
- name: Deploy to GitHub Pages
54+
id: deployment
55+
uses: actions/deploy-pages@v4

.github/workflows/release-build.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
run: ./mill "sjsonnet.jvm[$SCALA_VERSION].__.assembly"
2727
- name: JS Build
2828
run: ./mill "sjsonnet.js[$SCALA_VERSION].fullLinkJS"
29+
- name: Playground Build
30+
run: ./mill playground.bundle
2931
- name: Rename Artifacts
3032
run: |
3133
mkdir release
@@ -34,6 +36,7 @@ jobs:
3436
cp ./out/sjsonnet/jvm/$SCALA_VERSION/assembly.dest/out.jar ./release/sjsonnet-$VERSION.jar
3537
cp ./out/sjsonnet/jvm/$SCALA_VERSION/client/assembly.dest/out.jar ./release/sjsonnet-client-$VERSION.jar
3638
cp ./out/sjsonnet/jvm/$SCALA_VERSION/server/assembly.dest/out.jar ./release/sjsonnet-server-$VERSION.jar
39+
cp ./out/playground/bundle.dest/index.html ./release/sjsonnet-playground-$VERSION.html
3740
- uses: actions/upload-artifact@v6
3841
name: Upload Artifacts
3942
with:

build.mill

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,70 @@ object sjsonnet extends VersionFileModule {
375375
)
376376
}
377377
}
378+
379+
object playground extends Module {
380+
381+
def playgroundHtml = Task.Source(BuildCtx.workspaceRoot / "playground" / "index.html")
382+
383+
/**
384+
* Compile ScalaJS and inline the resulting JS bundle into a single HTML file. Note: the HTML
385+
* template (playground/index.html) may still reference external assets (e.g. CSS/JS from CDNs),
386+
* so the output is not guaranteed to be fully self-contained or offline-capable.
387+
*/
388+
def bundle = Task {
389+
val jsOutput = sjsonnet.js(scalaVersions.head).fullLinkJS()
390+
val mainJs = jsOutput.dest.path / "main.js"
391+
val dest = Task.ctx().dest
392+
393+
// Wrap CommonJS module for browser usage
394+
val source = os.read(mainJs)
395+
val wrappedJs =
396+
s"""// sjsonnet browser bundle - auto-generated
397+
|(function(global) {
398+
| var exports = {};
399+
| var module = { exports: exports };
400+
|
401+
| $source
402+
|
403+
| global.SjsonnetMain = exports.SjsonnetMain || module.exports.SjsonnetMain;
404+
|})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : Function('return this')());""".stripMargin
405+
406+
// Read HTML template and inline the JS bundle
407+
val htmlTemplate = os.read(playgroundHtml().path)
408+
val placeholder = "/* SJSONNET_BUNDLE_PLACEHOLDER */"
409+
val occurrences =
410+
java.util.regex.Pattern.quote(placeholder).r.findAllMatchIn(htmlTemplate).length
411+
require(
412+
occurrences == 1,
413+
s"Expected exactly 1 occurrence of '$placeholder' in playground/index.html, found $occurrences"
414+
)
415+
val inlinedHtml = htmlTemplate.replace(placeholder, wrappedJs)
416+
os.write(dest / "index.html", inlinedHtml)
417+
418+
PathRef(dest)
419+
}
420+
421+
/**
422+
* Build and open the playground in the default browser.
423+
*
424+
* Since the HTML is self-contained (JS inlined), no HTTP server is needed. The file is opened
425+
* directly in the default browser.
426+
*/
427+
def start() = Task.Command {
428+
val htmlFile = bundle().path / "index.html"
429+
val fileUri = htmlFile.toNIO.toUri.toString
430+
println(s"Opening Sjsonnet Playground: $fileUri")
431+
432+
val osName = System.getProperty("os.name", "").toLowerCase
433+
val openCmd =
434+
if (osName.contains("mac") || osName.contains("darwin")) Seq("open", fileUri)
435+
else if (osName.contains("win")) Seq("cmd", "/c", "start", "", fileUri)
436+
else Seq("xdg-open", fileUri) // Linux / BSD
437+
438+
val opened = scala.util.Try(os.proc(openCmd).call(stdout = os.Inherit, stderr = os.Inherit))
439+
if (opened.isFailure) {
440+
println(s"Could not open browser automatically. Please open this file manually:")
441+
println(s" $htmlFile")
442+
}
443+
}
444+
}

0 commit comments

Comments
 (0)