Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb9ac5d
initial setup and Hello World exercise
TheRealOwenRees Mar 7, 2026
1562c3e
update formatting
TheRealOwenRees Mar 7, 2026
6761c64
update placement of dependencies to allow tests to run
TheRealOwenRees Mar 7, 2026
2a15d84
remove ad ignore build artifacts
TheRealOwenRees Mar 7, 2026
0939121
pass verify_exercise script
TheRealOwenRees Mar 7, 2026
d592579
correct formatting
TheRealOwenRees Mar 7, 2026
1953c96
setup CI stage
TheRealOwenRees Mar 7, 2026
3a5d6cf
change testing command
TheRealOwenRees Mar 7, 2026
421ec24
update node_modules symlink
TheRealOwenRees Mar 7, 2026
bafa6a2
set track to inactive
TheRealOwenRees Mar 7, 2026
90bb5fa
update config with file slugs and interface file
TheRealOwenRees Mar 7, 2026
e03f731
update functions placeholder text and create interface file
TheRealOwenRees Mar 7, 2026
b37106e
added therealowenrees as co-author
TheRealOwenRees Mar 7, 2026
2229a38
set setup-nide action to a v4 commit
TheRealOwenRees Mar 7, 2026
e0e6464
interop used instead of interoperability in one title as the linter c…
TheRealOwenRees Mar 7, 2026
2bea052
reformat config
TheRealOwenRees Mar 7, 2026
3781718
fix typo in function name - exemplar
TheRealOwenRees Mar 8, 2026
7c0c5a1
typo fix in JavaScript
TheRealOwenRees Mar 8, 2026
9bcb8cd
skip tests, unskip when verifying exercise
TheRealOwenRees Mar 8, 2026
6381647
sync docs and config
TheRealOwenRees Mar 8, 2026
840ce52
basic test generator
TheRealOwenRees Mar 8, 2026
cf1f8fc
remove skipping of tests for now
TheRealOwenRees Mar 8, 2026
a808ea7
write test in generator, leaving template for a template string
TheRealOwenRees Mar 8, 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
21 changes: 7 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,20 @@ on:

jobs:
verify_exercises:
# TODO: replace with another image if required to run the tests (optional)
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

# TODO: setup any tooling that is required to run the tests (optional)
# E.g. install a specific version of a programming language
# E.g. install packages via apt/apk/yum/etc.
# Find GitHub Actions to setup tooling here:
# - https://github.com/actions/?q=setup&type=&language=
# - https://github.com/actions/starter-workflows/tree/main/ci
# - https://github.com/marketplace?type=actions&query=setup
# - name: Use <setup tooling>
# uses: <action to setup tooling>
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24.x
cache: "npm"

# TODO: install any dependencies (optional)
# E.g. npm install, bundle install, etc.
# - name: Install project dependencies
# run: <install dependencies>
- name: Install dependencies
run: npm ci

- name: Verify all exercises
run: bin/verify-exercises
5 changes: 5 additions & 0 deletions .gitignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .gitignore is for the students, correct? It probably should only be for build artifacts like node_modules then. There's extra stuff in here they wouldn't be using like configlet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be for this repo, without it I would end up commiting configlet and build artifacts. There is no .gitignore per exercise for the student it seems.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I thought this was in the hello world folder. We might as well give the students a .gitignore file in the exercise like the JS track does. At minimum, that means they won’t accidentally commit node_modules if they’re adding their solutions to a GitHub repo.

Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ bin/configlet.exe
bin/latest-configlet.tar.gz
bin/latest-configlet.zip
bin/configlet.zip
node_modules

tmp
exercises/**/.gitignore
lib
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "problem-specifications"]
path = problem-specifications
url = https://github.com/exercism/problem-specifications.git
75 changes: 75 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.DEFAULT_GOAL := test

EXERCISE ?= ""
EXERCISES = $(shell find ./exercises/practice -maxdepth 1 -mindepth 1 -type d | cut -s -d '/' -f4 | sort)
OUTDIR ?= "tmp"

# check all package.json and package-lock.json are matching
check-package-files:
@echo "Validation package.json files..."
@for pkg in $(PKG_FILES); do \
! ./bin/md5-hash $$pkg | grep -qv $(SOURCE_PKG_MD5) || { echo "$$pkg does not match main package.json. Please run 'make sync-package-files' locally and commit the results."; exit 1; }; \
done
@echo "Validation package-lock.json files..."
@for pkg in $(PKG_LOCK_FILES); do \
! ./bin/md5-hash $$pkg | grep -qv $(SOURCE_PKG_LOCK_MD5) || { echo "$$pkg does not match main package.json. Please run 'make sync-package-files' locally and commit the results."; exit 1; }; \
done
@echo "package-file check complete..."

# copy package.json and package-lock.json for single exercise
copy-package-file:
@cp package.json exercises/practice/$(EXERCISE)/package.json
@cp package-lock.json exercises/practice/$(EXERCISE)/package-lock.json
@cp rescript.json exercises/practice/$(EXERCISE)/rescript.json

# copy package files to all exercise directories
sync-package-files:
@echo "Syncing package.json and package-lock.json..."
@for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-package-file || exit 1; done

copy-exercise:
if [ -f exercises/practice/$(EXERCISE)/src/*.res ]; then \
echo "Copying $(EXERCISE)"; \
cp exercises/practice/$(EXERCISE)/.meta/*.res $(OUTDIR)/src/; \
cp exercises/practice/$(EXERCISE)/tests/*.res $(OUTDIR)/tests/; \
fi

copy-all-exercises:
@echo "Copying exercises for testing..."
@mkdir -p $(OUTDIR)/src
@mkdir -p $(OUTDIR)/tests
@for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-exercise || exit 1; done

# Remove the OUTDIR
clean:
@echo "Cleaning tmp directory..."
@rm -rf $(OUTDIR)

# Format all ReScript files in the project
format:
@echo "Formatting ReScript files..."
@find . -name "node_modules" -prune -o -name "*.res" -print -o -name "*.resi" -print | xargs npx rescript format

generate-tests:
@echo "Generating tests for all exercises..."
@for exercise in $(EXERCISES); do \
if [ -f exercises/practice/$$exercise/.meta/generateTests.js ]; then \
echo "-> Generating: $$exercise"; \
node exercises/practice/$$exercise/.meta/generateTests.js || exit 1; \
else \
echo "-> Skipping: $$exercise (no generator found)"; \
fi \
done
@echo "All tests generated successfully."

generate-test:
ifeq ($(EXERCISE),"")
$(error EXERCISE variable is required. usage: make generate_test EXERCISE=hello-world)
endif
@node exercises/practice/$(EXERCISE)/.meta/generateTests.js

test:
$(MAKE) -s clean
$(MAKE) -s check-package-files
$(MAKE) -s copy-all-exercises
npm run ci
30 changes: 11 additions & 19 deletions bin/verify-exercises
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ required_tool() {

required_tool jq

copy_example_or_examplar_to_solution() {
copy_example_or_exemplar_to_solution() {
jq -c '[.files.solution, .files.exemplar // .files.example] | transpose | map({src: .[1], dst: .[0]}) | .[]' .meta/config.json \
| while read -r src_and_dst; do
cp "$(jq -r '.src' <<< "${src_and_dst}")" "$(jq -r '.dst' <<< "${src_and_dst}")"
Expand All @@ -30,45 +30,37 @@ copy_example_or_examplar_to_solution() {

unskip_tests() {
jq -r '.files.test[]' .meta/config.json | while read -r test_file; do
noop # TODO: replace this with the command to unskip the tests.
# Note: this function runs from within an exercise directory.
# Note: the exercise directory is a temporary directory, so feel
# free to modify its (test) files as needed.
# Note: ignore this function if either:
# - skipping tests is not supported, or
# - skipping tests does not require modifying the test files.
# Example: sed -i 's/test.skip/test/g' "${test_file}"
sed -i 's/skip(/test(/g' "${test_file}" # replace skip utility function name with test
done
}

run_tests() {
noop # TODO: replace this with the command to run the tests for the exercise.
# Note: this function runs from within an exercise directory.
# Note: the exercise directory is a temporary directory, so feel
# free to modify its files as needed.
# Note: return a zero exit code if all tests pass, otherwise non-zero.
# Example: `npm test`
# Example: `python3 -m pytest two_fer_test.py`
node_modules/.bin/rescript
node_modules/.bin/retest tests/*.js
}

verify_exercise() {
local dir
local slug
local tmp_dir
local root_dir

root_dir=$(realpath .) # capture project root before cd-ing away
dir=$(realpath "${1}")
slug=$(basename "${dir}")
tmp_dir=$(mktemp -d -t "exercism-verify-${slug}-XXXXX")

echo "Verifying ${slug} exercise..."

(
trap 'rm -rf "$tmp_dir"' EXIT # remove tempdir when subshell ends
trap 'rm -rf "$tmp_dir"' EXIT
cp -r "${dir}/." "${tmp_dir}"
cd "${tmp_dir}"

copy_example_or_examplar_to_solution
unskip_tests
ln -s "${root_dir}/node_modules" "${tmp_dir}/node_modules"

copy_example_or_exemplar_to_solution
# unskip_tests
run_tests
)
}
Expand Down
86 changes: 75 additions & 11 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,87 @@
"representer": false,
"analyzer": false
},
"blurb": "TODO: add blurb",
"blurb": "ReScript is a strongly typed functional language which compiles to both Javascript and native.",
"version": 3,
"online_editor": {
"indent_style": "space",
"indent_size": 4
"indent_size": 2,
"highlightjs_language": "reasonml"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How close is the ReasonML syntax for highlighting purposes? https://github.com/tsnobip/highlightjs-rescript might be a possibility. Exercism can load that NPM package or we can even spin up our own if we’re feeling particularly motivated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

about 90% of the ReasonML syntax highlighting should work, but then it will fail on piping and parametric polymorphism. The package you linked looks good.

},
"test_runner": {
"average_run_time": 7
},
"files": {
"solution": [],
"test": [],
"example": [],
"exemplar": []
"solution": [
"src/%{pascal_slug}.res"
],
"test": [
"tests/%{pascal_slug}_test.res"
],
"example": [
".meta/%{pascal_slug}.res"
],
"exemplar": [
".meta/src/%{pascal_slug}.res"
],
"editor": [
"src/%{pascal_slug}.resi"
]
},
"exercises": {
"concept": [],
"practice": []
"practice": [
{
"slug": "hello-world",
"name": "Hello World",
"uuid": "63d72b81-c7a4-4a94-8224-69f4c68cc374",
"practices": [],
"prerequisites": [],
"difficulty": 1
}
]
},
"concepts": [],
"key_features": [],
"tags": []
"key_features": [
{
"title": "OCaml's type system",
"content": "ReScript brings OCaml's battle-tested powerful type system to JavaScript.",
"icon": "stable"
},
{
"title": "Inferred types",
"content": "Types are inferred by the ReScript compiler and are guaranteed to be correct.",
"icon": "immutable"
},
{
"title": "Functional Programming",
"content": "Like OCaml, ReScript is a functional programming language with pattern matching, variants and more.",
"icon": "functional"
},
{
"title": "JavaScript interop",
"content": "JavaScript interoperability is easy, allowing existing JavaScript packages to be used.",
"icon": "interop"
},
{
"title": "Fast compiler",
"content": "ReScript compilation times are super fast which means fast iteration cycles.",
"icon": "fast"
},
{
"title": "Refactor with ease",
"content": "Compiler guides you through all places that need to be fixed during refactor until it just works.",
"icon": "fun"
}
],
"tags": [
"execution_mode/compiled",
"paradigm/functional",
"platform/linux",
"platform/mac",
"platform/web",
"platform/windows",
"typing/static",
"typing/strong",
"used_for/frontends",
"used_for/web_development"
]
}
16 changes: 16 additions & 0 deletions exercises/practice/hello-world/.docs/instructions.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an old set of instructions. We don't need to do it right this moment, but before launch, let's make sure to have configlet sync docs and metadata for everything.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Instructions

The classical introductory exercise.
Just say "Hello, World!".

["Hello, World!"][hello-world] is the traditional first program for beginning programming in a new language or environment.

The objectives are simple:

- Modify the provided code so that it produces the string "Hello, World!".
- Run the test suite and make sure that it succeeds.
- Submit your solution and check it at the website.

If everything goes well, you will be ready to fetch your first real exercise.

[hello-world]: https://en.wikipedia.org/wiki/%22Hello,_world!%22_program
1 change: 1 addition & 0 deletions exercises/practice/hello-world/.meta/HelloWorld.res
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let hello = () => "Hello, World!"
29 changes: 29 additions & 0 deletions exercises/practice/hello-world/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"authors": [
"tejasbubane",
"therealowenrees"
],
"contributors": [
"naartjie",
"ohana54",
"son1112",
"stevejb71"
],
"files": {
"solution": [
"src/HelloWorld.res"
],
"test": [
"tests/HelloWorld_test.res"
],
"example": [
".meta/HelloWorld.res"
],
"editor": [
"src/HelloWorld.resi"
]
},
"blurb": "Exercism's classic introductory exercise. Just say \"Hello, World!\".",
"source": "This is an exercise to introduce users to using Exercism",
"source_url": "https://en.wikipedia.org/wiki/%22Hello,_world!%22_program"
}
11 changes: 11 additions & 0 deletions exercises/practice/hello-world/.meta/generateTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import getValidCases from '../../../../test_generator/getCases.js';
import { generate, toPascalCase } from '../../../../test_generator/testGenerator.js';
import * as template from './testTemplate.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const cases = getValidCases(template.slug);
const outputPath = path.resolve(__dirname, '..', 'tests', `${toPascalCase(template.slug)}_test.res`);

generate(outputPath, template.slug, cases, template);
8 changes: 8 additions & 0 deletions exercises/practice/hello-world/.meta/testTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const slug = 'hello-world';

export const assertionFunctions = `let stringEqual = (~message=?, a: string, b: string) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to build and document a small library of common assertion functions that can be pulled into templates? We can still have custom functions for more complicated assertions, but then we don’t need to reinvent things like checking if a returned value is true of not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think this would be a good idea. I will scour through the rest of the exercises already implemented for this track in my repo and add those assertions for now, and add more as we move forward

assertion(~message?, ~operator="stringEqual", (a, b) => a == b, a, b)`;

export const template = (c, moduleName) => {
return `stringEqual(~message="${c.description}", ${moduleName}.hello(), "${c.expected}")`
}
13 changes: 13 additions & 0 deletions exercises/practice/hello-world/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[af9ffe10-dc13-42d8-a742-e7bdafac449d]
description = "Say Hi!"
21 changes: 21 additions & 0 deletions exercises/practice/hello-world/LICENSE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this LICENSE file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we do because of the package JSON putting this under a MIT license. At some point, we’ll want a script that runs configlet create and then also copies over the additional files we need like this and the package.json. That’s not an immediate need but the sooner we do it, the less chance for human error. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would that be the sync command ? Where do we normally place such a script?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bin is usually where that goes. I think we should create a template folder at the repo root that has these extra files in the relative spots where they are expected to go in the final exercise folder. Then you can copy the template folder to the exercise folder after configlet create does its thing.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Exercism

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading