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
116 changes: 83 additions & 33 deletions apps/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,33 @@ contract only. Lesson copy belongs to the guide author.

## Layout

| Path | Purpose |
| --------------------------------- | ------------------------------------------------ |
| `app/lessons/` | Lesson MDX files and metadata |
| `app/GuideLessonContent.res` | Build-time lesson discovery and validation |
| `app/GuideHome.res` | Guide workspace UI |
| `app/GuideCompilerBridgeHook.res` | Browser compiler and execution integration |
| `styles/main.css` | Guide-only visual system and responsive behavior |
| `public/` | Guide static assets, including `robots.txt` |
| `__tests__/` | Browser and unit coverage for the guide |
| Path | Purpose |
| --------------------------------- | -------------------------------------------------- |
| `app/lessons/` | Lesson MDX files and metadata |
| `app/GuideLessonContent.res` | Build-time lesson discovery and validation |
| `app/GuideHome.res` | Guide workspace UI |
| `app/GuideCompilerBridgeHook.res` | Browser compiler and execution integration |
| `app/GuideLessonValidation.res` | Build-time compilation and reference output checks |
| `styles/main.css` | Guide-only visual system and responsive behavior |
| `public/` | Guide static assets, including `robots.txt` |
| `__tests__/` | Browser and unit coverage for the guide |

## Lesson Contract

Every `.mdx` file under `app/lessons/` is discovered recursively and ordered by
its integer `position`. The build requires these frontmatter fields:

| Field | Required | Meaning |
| ------------------------- | -------- | ------------------------------------------------------ |
| `position` | Yes | Signed 32-bit integer lesson order |
| `id` | Yes | Stable URL-safe hash and lesson identifier |
| `missionLabel` | Yes | Short lesson label |
| `title` | Yes | Lesson heading |
| `exercise.id` | Yes | Stable identifier for saved code and completion |
| `exercise.initialCode` | Yes | Initial editor contents |
| `exercise.expectedOutput` | Yes | Matching runtime log line that unlocks the next lesson |
| Field | Required | Meaning |
| ------------------------------------ | -------- | -------------------------------------------------------- |
| `position` | Yes | Signed 32-bit integer lesson order |
| `id` | Yes | Stable URL-safe hash and lesson identifier |
| `missionLabel` | Yes | Short lesson label |
| `title` | Yes | Lesson heading |
| `exercise.id` | Yes | Stable identifier for saved code and completion |
| `exercise.initialCode` | Yes | Initial editor contents |
| `exercise.expectedOutput` | Yes | Matching runtime log line that unlocks the next lesson |
| `exercise.solutionCode` | No | Build-only reference solution; defaults to `initialCode` |
| `exercise.expectedInitialDiagnostic` | No | Build-only expected syntax/type error in the starter |

The build rejects malformed fields and duplicate positions, lesson IDs, or
exercise IDs. Errors identify the source lesson file and field. Source paths
Expand All @@ -50,10 +53,45 @@ Lesson IDs must be URL-safe slugs made from lowercase ASCII letters and digits,
with single hyphens between words. Exercise IDs may contain one or more such
slugs separated by single slashes, such as `first-contact/greeting`.

Every exercise requires a deterministic `expectedOutput` string. Numeric or
boolean output must be quoted in YAML, for example `expectedOutput: "42"`.
Matching is exact: empty strings and whitespace are preserved. Manual checks
are not supported.
Every published exercise needs a deterministic `expectedOutput` string. The
build checks `initialCode` and compiles `solutionCode`, runs the reference solution,
and requires a matching runtime log line. Matching preserves whitespace and
uses the same argument serialization as the browser checkpoint.

Numeric or boolean output must be quoted in YAML, for example
`expectedOutput: "42"`. Matching is exact: empty strings and whitespace are
preserved. Manual checks are not supported.

When the learner must edit the starter to reach the checkpoint, supply
`exercise.solutionCode`. Otherwise it defaults to `initialCode`. Reference
solutions are only read during validation and are excluded from published
lesson data. Solutions must produce synchronous output and finish within one
second; browser-only APIs and asynchronous exercises need a separate validation
strategy before they can be authored.

For an exercise that deliberately starts with a compiler error, set
`exercise.expectedInitialDiagnostic` to a non-empty, case-sensitive substring
of the compiler's diagnostic message and provide a separate `solutionCode`:

```yaml
exercise:
id: types/fix-a-mismatch
initialCode: |
let answer: int = false
expectedInitialDiagnostic: "But it's expected to have type: int"
solutionCode: |
let answer: int = 42
expectedOutput: "42"
```

The starter must fail with syntax or type errors, and every reported error's
message must contain the configured text. Matching ignores ANSI formatting and
does not search source excerpts or line numbers. If the starter compiles, reports
a different error, or encounters a compiler/configuration failure, the build
fails. The reference solution must still compile, run successfully, and produce
the exact checkpoint output. Both validation fields are omitted from published
lesson data; learners still complete the exercise by fixing the code and
producing `expectedOutput`.

## Local Development

Expand All @@ -76,17 +114,28 @@ yarn build:guide
yarn workspace @rescript-lang/guide ci:test
```

The CI test command validates frontmatter and the complete lesson collection
in Node before running the browser tests. After compiling ReScript, run
`yarn workspace @rescript-lang/guide test:lessons` for just the lesson checks.

`yarn build:guide` compiles ReScript and pre-renders `/` into
`apps/guide/build/client/`, the assets directory configured in `wrangler.toml`.
Vite serves and copies the guide's `public/` directory, including `robots.txt`.
The root route imports the shared docs favicons so Vite emits them with the
guide's other assets. The guide fetches available compiler versions from
`https://cdn.rescript-lang.org` during pre-rendering; the
interactive compiler and runtime then run in the learner's browser.
`yarn build:guide` compiles ReScript, validates the lessons during pre-rendering,
and pre-renders `/` into `apps/guide/build/client/`, the assets directory configured
in `wrangler.toml`. Vite serves and copies the guide's `public/` directory,
including `robots.txt`. The root route imports the shared docs favicons so Vite
emits them with the guide's other assets.

Validation uses the actual browser compiler and its libraries from `https://cdn.rescript-lang.org`.
A failed download, compilation, execution, or output check stops the build.
`PLAYGROUND_BUNDLE_ENDPOINT` can override the CDN base URL.

The guide pins ReScript **v12.3.1** in `app/GuideCompilerSettings.res`. Both
validation and browser execution use that version and its runtime files; new
CDN releases cannot silently change lessons. To upgrade, change the pin and run
`yarn workspace @rescript-lang/guide test:compiler` and `yarn build:guide`.
The CI test command validates frontmatter, the complete lesson collection, and
compiler output in Node before running the browser tests. After compiling
ReScript, run `yarn workspace @rescript-lang/guide test:lessons` for just the
lesson authoring checks, or `yarn workspace @rescript-lang/guide test:compiler`
for the compiler checks.

In the browser, the output panel reports a failed compiler download or a
15-second loading timeout with instructions to reload.

## Current Capability

Expand All @@ -107,6 +156,7 @@ show a stop screen rather than a usable guide.
Track outstanding guide launch work in this checklist:

- [ ] Author, review, and run every lesson using its real expected output.
- [x] Compile starters and check reference solution output against a pinned compiler.
- [x] Add a content-validation test for the complete lesson set, including
unique `position`, lesson IDs, and exercise IDs.
- [ ] Decide and implement the completion model for exercises that cannot use
Expand Down
47 changes: 47 additions & 0 deletions apps/guide/__tests__/GuideCompilerLoading_.test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
open Vitest

@module("./compilerFixture.js")
external waitForCompilerRequest: unit => promise<unit> = "waitForCompilerRequest"
@module("./compilerFixture.js") external failCompilerLoading: unit => unit = "failCompilerLoading"

@module("./compilerLoadingFixture.js")
external expireCompilerLoading: unit => unit = "expireCompilerLoading"

let renderWithCompiler = () =>
render(
<ReactRouter.MemoryRouter>
<GuideHome
lessons=GuideTestFixtures.guideLessons
compilerData={bundleBaseUrl: "https://guide-compiler.test"}
/>
</ReactRouter.MemoryRouter>,
)

test("shows a compiler loading failure in the guide output", async () => {
await viewport(1440, 900)
let screen = await renderWithCompiler()
await waitForCompilerRequest()
failCompilerLoading()
await (await screen->getByText("Compiler setup failed"))->element->toBeVisible
await (
await screen->getByText(
"Could not load compiler from url https://guide-compiler.test/v12.3.1/compiler.js",
)
)
->element
->toBeVisible
})

test("shows a useful message when the compiler CDN never responds", async () => {
await viewport(1440, 900)
expireCompilerLoading()
let screen = await renderWithCompiler()
await (await screen->getByText("Compiler setup failed"))->element->toBeVisible
await (
await screen->getByText(
"ReScript v12.3.1 did not load within 15 seconds. Reload the page to try again.",
)
)
->element
->toBeVisible
})
12 changes: 3 additions & 9 deletions apps/guide/__tests__/GuideCompilerSettings_.test.res
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
open Vitest

test("selects the latest stable ReScript compiler with ESM output", async () => {
let version =
["v12.1.0-alpha.1", "v11.1.4", "v12.0.0", "v12.2.0-beta.1", "v12.1.3"]
->GuideCompilerSettings.supportedVersions
->GuideCompilerSettings.latestStableParsedVersion
->Option.getOrThrow
->Semver.toString

expect(version)->toBe("v12.1.3")
test("pins the guide to its validated v12 compiler with ESM output", async () => {
expect(GuideCompilerSettings.version)->toBe("v12.3.1")
expect(GuideCompilerSettings.parsedVersion->Semver.toString)->toBe(GuideCompilerSettings.version)
expect(GuideCompilerSettings.moduleSystem)->toBe("esmodule")
expect(GuideCompilerSettings.warnFlags->String.includes("-109"))->toBe(true)
})
1 change: 0 additions & 1 deletion apps/guide/__tests__/GuideHome_.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ afterEach(clearGuideState)

let compilerData: GuideCompilerData.t = {
bundleBaseUrl: "/test-compiler",
versions: ["12.2.0"],
}

test("loads saved guide editor code into the editor", async () => {
Expand Down
Loading