Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
282e8fc
Fix incorrect doc tags
leeyi45 Dec 23, 2025
1f12e20
Add validation for example tags and hide type guards in generated docs
leeyi45 Dec 23, 2025
3370426
Update docs about documentation
leeyi45 Dec 23, 2025
175ab4e
Continue updating docs
leeyi45 Dec 25, 2025
1dd8cde
Add linting for code examples
leeyi45 Dec 25, 2025
bbc6970
Add compile command
leeyi45 Dec 25, 2025
5754a29
Fix bug where markdown directory trees weren't being aligned properly
leeyi45 Dec 28, 2025
54c4eed
Add tests for the markdown tree transformer
leeyi45 Dec 28, 2025
49cbc9a
Run format
leeyi45 Dec 28, 2025
44d5dc3
Move buildtools to use tsc compile and typecheck separately
leeyi45 Jan 10, 2026
ddcd384
Add tests for ensuring that typedoc paths are processed correctly
leeyi45 Jan 10, 2026
5a8b123
Add tests to ensure that typeguards are properly rendered
leeyi45 Jan 10, 2026
e87456f
Add the compile command to bundles
leeyi45 Jan 10, 2026
8a1f40c
Update eslint-plugin-json version
leeyi45 Jan 10, 2026
68dee49
Update code examples to comply with linting
leeyi45 Jan 10, 2026
989eba9
Fix code example from plotly
leeyi45 Jan 10, 2026
f44cfba
Update vitest eslint plugin
leeyi45 Jan 10, 2026
d0b1422
Change reference to tsc command to instead refer to compile command
leeyi45 Jan 10, 2026
7bca240
Fix incorrect eslint rules
leeyi45 Jan 15, 2026
875adf5
Merge remote-tracking branch 'origin/master' into plugins
leeyi45 Jan 21, 2026
7afa0fd
Update typescript-eslint versions
leeyi45 Jan 21, 2026
7d3a5b4
Fix spelling
leeyi45 Jan 21, 2026
5794b53
Update blueprintjs versions and add it to packages that didn't specif…
leeyi45 Jan 23, 2026
6947174
Fix eslint import paths
leeyi45 Jan 23, 2026
915d8e8
Add lodash and @types/lodash to packages that were missing them
leeyi45 Jan 23, 2026
22e2127
Disable import/no-unresolved when running in CI
leeyi45 Jan 23, 2026
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
1 change: 1 addition & 0 deletions .github/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"devDependencies": {
"@sourceacademy/modules-repotools": "workspace:^",
"@types/lodash": "^4.14.198",
"@types/node": "^22.15.30",
"typescript": "^5.8.2",
"vitest": "^4.0.4"
Expand Down
6 changes: 3 additions & 3 deletions devserver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
"devserver": "./bin.js"
},
"dependencies": {
"@blueprintjs/core": "^5.10.2",
"@blueprintjs/icons": "^5.9.0",
"@blueprintjs/core": "^6.0.0",
"@blueprintjs/icons": "^6.0.0",
"@commander-js/extra-typings": "^14.0.0",
"@sourceacademy/modules-lib": "workspace:^",
"@vitejs/plugin-react": "^5.1.0",
"ace-builds": "^1.25.1",
"classnames": "^2.3.1",
"commander": "^14.0.0",
"js-slang": "^1.0.81",
"js-slang": "^1.0.85",
"re-resizable": "^6.9.11",
"react": "^18.3.1",
"react-ace": "^10.1.0",
Expand Down
2 changes: 1 addition & 1 deletion devserver/src/components/sideContent/SideContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface TabIconProps {
function TabIcon({ iconName, tooltip, shouldAlert }: TabIconProps) {
return <Tooltip content={tooltip}>
<div className={!shouldAlert ? 'side-content-tooltip' : 'side-content-tooltip side-content-tab-alert'}>
<Icon icon={iconName} iconSize={20} />
<Icon icon={iconName} size={20} />
</div>
</Tooltip>;
}
Expand Down
32 changes: 31 additions & 1 deletion docs/src/buildtools/5-builders/2-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Firstly, the `buildtools build docs` is called on each bundle. This produces two
1. `build/jsons/[bundle_name].json`, which is the JSON documentation used by `js-slang` and the frontend
2. `docs.json`, which is JSON documentation format used by `typedoc` to generate the HTML documentation later

Second, `buildtools manifest` is called, which then takes the `docs.json` files from each bundle and uses the [`merge` entry point strategy](https://typedoc.org/documents/Options.Input.html#merge) to create the HTML
Second, `buildtools html` is called, which then takes the `docs.json` files from each bundle and uses the [`merge` entry point strategy](https://typedoc.org/documents/Options.Input.html#merge) to create the HTML
documentation

> [!NOTE]
Expand All @@ -20,6 +20,36 @@ documentation

This allows each bundle to have its own Typedoc option set.

## Custom Checks

There are two important custom checks that our documentation generation must perform:

### Typing Variables as Functions

Because Typedoc no longer automatically considers function-like types as functions, developers must use the `@function` tag to allow
Typedoc to properly infer that a function-like variable should be documented as a function.

When generating the documentation, we simply check if the corresponding item has been assigned a variable type while having function signatures. If this
is the case, then we print a warning advising the developer that they may have forgotten the `@function` tag.

### Hiding Type Guards

Typescript has a feature called type guards that allow the compiler to better narrow types, increasing the type safety of module code.

When Typedoc encounters a type guard, it documents the return type of that type guard as is.

```ts
export function is_boolean(obj: unknown): obj is boolean {
return typeof obj === 'boolean';
}
```

The function above has its return type rendered as `obj is boolean`. However, this might confuse cadets as type guards are not a concept in Source,
and the return type as rendered also hides the fact that `is_boolean` returns a `boolean` and not some other special value.

To remedy this, when generating the documentation for type guards, we simply replace the return type of the signature with `boolean` (or `void` if
it's an asserts guard). This modification is done on "conversion" by hooking into the `Converter.EVENT_CREATE_SIGNATURE` event.

## HTML Generation

It is not possible to generate the HTML documentation on a per-bundle basis. Thus, when HTML documentation needs to be regenerated, the source files of every single bundle needs to be processed.
Expand Down
54 changes: 54 additions & 0 deletions docs/src/modules/2-bundle/4-conventions/1-basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This section contains some conventions to follow when writing your bundle.

[[toc]]

## 1. Cadet facing functions should not have default or rest parameters

The function signature below takes in two booleans, the second of which is optional. This is not supported for Module functions in Source, but is fine if your function
Expand Down Expand Up @@ -211,3 +213,55 @@ Note that not every export from `js-slang` is currently supported. Below is the
- `js-slang/dist/utils/stringify`
- `js-slang/dist/parser/parser`
- `js-slang/dist/cse-machine/interpreter`

## 6. Cadet Facing Type Guard Conventions

A [type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) is a function that checks if the provided object has the desired type.
In Typescript, there are two kinds of type guards. Cadet facing type guards should favour the boolean returning form and should begin with the
`is` prefix. Also, the parameter the type guard validates should be of type `unknown`.

Anywhere you use a boolean check followed by a type assertion:
```ts
// Simple boolean type guard
export function is_sound(obj: unknown): boolean {
return obj instanceof Sound;
}

function play(obj: Sound) {
// Simple boolean check
if (!is_sound(obj)) return false;

// followed by type assertion, since obj is still of type unknown
const sound = obj as Sound;
// ...implementatation details
}
```

you should replace with a type guard:
```ts
// Typescript boolean Type Guard
export function is_sound(obj: unknown): obj is Sound {
return obj instanceof Sound;
}

function play(obj: Sound) {
// Check using a type guard
if (!is_sound(obj)) return false;

// Type assertion is no longer required. Typescript knows that obj
// is a Sound here

// ...implementatation details
}
```

Note that for simple checks (like `instanceof`), Typescript (later versions) is able to automatically infer when you are writing a type guard,
meaning that explicitly typing your type guard may be unnecessary.

However, if the checks you need to perform are complex, Typescript might
only be able to narrow the return type of your type guard to `boolean`. In such a case you will have to write the return type explicitly.

> [!TIP] Type Guards in Documentation
> Type guards have a specific syntax for their return types (e.g. `value is null`) that isn't Source compliant.
> These types get replaced with `boolean` when the documentation is generated, so you don't have to worry
> about type guard types appearing in your documentation.
74 changes: 73 additions & 1 deletion docs/src/modules/2-bundle/5-documentation/1-cadet/1-cadet.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Documentation should be written in [Markdown](https://www.markdownguide.org/gett
the markdown is converted to raw HTML.

::: details Type Aware annotations

[JSDoc](https://jsdoc.app) (and TSDoc) both support annotations that express type information directly like `@type` or annotations that can optionally contain type information like `@param` and `@returns`.
Since modules are already written in Typescript, there is no need to use type-aware annotations to document the type of an object.

Expand Down Expand Up @@ -131,7 +132,8 @@ export const draw_connected = createDrawFunction('none', 'lines', '2D', false);
The export will now be correctly recognized as a function:
![](./drawFunc.png)

There is no automatic way to make this distinction, so it is up to the bundle authors to make sure that this convention is adhered to.
The buildtools are configured to emit a warning if a variable is detected to have function signatures but cannot automatically rectify
this problem.

### Variables/Constants

Expand Down Expand Up @@ -195,11 +197,81 @@ This causes `type_map` to be removed from the documentation, even if it is expor
> Bundle `type_map`s are supposed to be internal implementation details hidden from users. If you forget to apply a `@hidden` tag to
> your bundle's type map export, the build tools will show a warning.

### Use of `@example`

`@example` blocks allow the developer to provide code snippets that serve as examples for for how the function/variable is intended to be
used. Consider an example from the `midi` bundle below:

```ts {10-13}
/**
* Converts a letter name to its corresponding MIDI note.
* The letter name is represented in standard pitch notation.
* Examples are "A5", "Db3", "C#7".
* Refer to <a href="https://i.imgur.com/qGQgmYr.png">this</a> mapping from
* letter name to midi notes.
*
* @param note given letter name
* @return the corresponding midi note
* @example
* ```
* letter_name_to_midi_note('C4'); // Returns 60
* ```
* @function
*/
export function letter_name_to_midi_note(note: NoteWithOctave): MIDINote {}
```

When the documentation is renderered, a code block is produced:

![](./docExample.png)

Note that your code examples should be surrounded in a Markdown code block (using the triple backticks ```` ``` ````). This will
help Typedoc figure out what content belongs to your code block. The language specifier is not required here as it is assumed that
your code examples are written in Typescript.

::: details Why you should use a code block

If you use an inline code example, Typedoc might include things in your example that you didn't mean to include. Consider
a modified version of the example above:

```ts {10}
/**
* Converts a letter name to its corresponding MIDI note.
* The letter name is represented in standard pitch notation.
* Examples are "A5", "Db3", "C#7".
* Refer to <a href="https://i.imgur.com/qGQgmYr.png">this</a> mapping from
* letter name to midi notes.
*
* @param note given letter name
* @return the corresponding midi note
* @example letter_name_to_midi_note("C4"); // Returns 60
* @function
*/
export function letter_name_to_midi_note(note: NoteWithOctave): MIDINote {}
```

Typedoc ends up including the `@function` tag as part of the code block:

![](./wrongDocExample.png)

Using a code block makes it clear exactly what is intended to be part of your example code block.
:::

During documentation generation, the code in your code block will be parsed by a Typescript parser to ensure that you have written
valid Typescript code. It will print a warning message if your example code doesn't produce syntactically valid Typescript.

### Other Tags

There are a variety of tags that Typedoc supports. This list can be found [here](https://typedoc.org/documents/Tags.html). When writing your documentation you should use these tags
to the best of your ability to help make your documentation as comprehensive as possible.

> [!INFO] Configuring Supported Tags
> There is an ESLint rule configured to error when you use an unknown tag. By default, the rule includes all tags supported by JSDoc, but Typedoc
> actually supports many more tags intended for customizing documentaton output.
>
> If you want to use a Typedoc supported tag that hasn't been configured for use, you can simply modify the `jsdoc/check-tag-names`
> rule to include your tag.

## HTML Documentation

The human readable documentation resides in the `build/documentation` folder. You can view its output [here](https://source-academy.github.io/modules/documentation). This is what the output for `make_point` looks like:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/src/modules/2-bundle/6-compiling.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If you intend for your bundle to be consumed from other bundles, do the followin
"./*": "./dist/*.js"
},
"scripts": {
"postinstall": "yarn buildtools tsc"
"postinstall": "buildtools compile"
}
}
```
Expand Down
11 changes: 10 additions & 1 deletion docs/src/repotools/2-linting.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ When changing a rule's configuration, leave a comment explaining what the origin

Where possible, also provide an explanation for why that rule has been configured as such:

<<< ../../../eslint.config.js#typescript {21}
<<< ../../../eslint.config.js#typescript {23}

## Linting Markdown Code Examples

Expand All @@ -35,6 +35,15 @@ provides a processor that makes these code blocks available to ESLint.
Because `typescript-eslint` requires a `tsconfig` to be able to lint any kind of Typescript code, it can't actually be used with any type-aware rules. Furthermore, code examples
are usually incomplete snippets of Typescript/Javascript, so a lot of the typical rules in use don't really apply to these. Thus, code examples have their own separate configurations.

## Linting JSDoc Code Examples

The `eslint-plugin-jsdoc` package provides an ESLint processor that can be used to extract `@example` code blocks and process them for linting. The processor actually
considers the example code blocks as Markdown code examples, so the same block used for configuring those is also used to configure JSDoc code examples.

Similar to Markdown code blocks, the type-aware rules also do not work with JSDoc code examples.

The code example processor is configured to only lint code examples that are contained within Markdown code blocks.

## Linting JSON Files

For the most part, there are only stylistic rules that need to be applied to JSON files. This is why this repository doesn't use the official `@eslint/json` package for linting JSON files.
Expand Down
24 changes: 22 additions & 2 deletions docs/src/repotools/6-docserver/1-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@ Originally most of the developer documentation was contained within the Github r

This documentation server is powered by [Vitepress](https://vitepress.dev), which takes Markdown files and renders them into a (mostly) static site.

## File Structure
## Base URL

The entire developer documentation server is designed to be deployed at [](https://source-academy.github.io/modules/devdocs), which means that all links throughout
the docserver's source code will be calculated relative to this URL. This setting is controlled by the `base` option in the Vitepress config file.

## Directory Structure

All pages for the server are contained under `src`.

[`vitepress-sidebar`](https://vitepress-sidebar.cdget.com) is being used to generate the sidebar for the server. This means that entries in the sidebar appear sorted according to the file/folder names from which they originate. This is why some folder have contents that are labelled `1-something`, `2-something` etc. Each item's value is taken from file's frontmatter, or if not present, the file's first header.

Folders produce item groups, the name of which can be customized using an `index.md` file within that folder.
If there is a file with the same name as its parent folder, then that file is used as the target for the path. For example:

```dirtree
name: 1-bundles
children:
- 1-bundles.md
- name: example.png
comment: Picture that's displayed in 1-bundles.md
```

then the link `./1-bundles.html` will automatically display the `1-bundles.md` page. Using a folder this way lets you group assets intended for a single page.

If instead you use an `index.md` page, then the the sidebar displays a menu group, the title of which is taken from the `index.md` page.
For example:

```dirtree
name: bundles
Expand Down Expand Up @@ -46,6 +64,8 @@ children:
The above file structure produces the menu below:
![image](./menu.png)

Each page can then be referred to using `./bundles/1-getting-started/1-overview.html` or `./bundles/2-bundle/1-overview.html` etc.

Each item takes its title value from either the frontmatter (if available) or the first heading of each page.

`index.md` files are only used to title the menus, they are not intended for navigation, though are navigable to if the user enters the address manually, so it is still recommended to keep some basic documentation in those pages or create rewrites so that users are automatically redirected away from those pages. Here are the contents of <nobr><code>2-bundle/index.md</code></nobr>:
Expand Down
Loading
Loading