diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index 90300f5..1ddbda9 100644
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -1,13 +1,24 @@
#!/usr/bin/env bash
set -e
-filesToFormat=$(
+mixFiles=$(
git --no-pager diff --name-status --no-color --cached | \
awk '$1 != "D" && $2 ~ /\.exs?$/ {print $NF}'
)
-for path in $filesToFormat
+for path in $mixFiles
do
mix format "$path"
git add "$path"
done
+
+webFiles=$(
+ git --no-pager diff --name-status --no-color --cached | \
+ awk '$1 != "D" && $2 ~ /\.jsx?$|\.tsx?$/ {print $NF}'
+)
+
+for path in $webFiles
+do
+ prettier --write "$path"
+ git add "$path"
+done
diff --git a/assets/js/react/App.jsx b/assets/js/react/App.tsx
similarity index 100%
rename from assets/js/react/App.jsx
rename to assets/js/react/App.tsx
diff --git a/assets/js/react/main.jsx b/assets/js/react/main.tsx
similarity index 57%
rename from assets/js/react/main.jsx
rename to assets/js/react/main.tsx
index 2cd2522..9c02848 100644
--- a/assets/js/react/main.jsx
+++ b/assets/js/react/main.tsx
@@ -3,5 +3,4 @@ import * as React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
-ReactDOM.createRoot(document.getElementById('mount')).render()
-
+ReactDOM.createRoot(document.getElementById('mount')!).render()
diff --git a/assets/package-lock.json b/assets/package-lock.json
new file mode 100644
index 0000000..4275da8
--- /dev/null
+++ b/assets/package-lock.json
@@ -0,0 +1,139 @@
+{
+ "name": "assets",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.40",
+ "@types/react-dom": "^18.2.17",
+ "typescript": "^5.3.2"
+ }
+ },
+ "node_modules/.pnpm/js-tokens@4.0.0/node_modules/js-tokens": {
+ "version": "4.0.0",
+ "extraneous": true,
+ "license": "MIT",
+ "devDependencies": {
+ "coffeescript": "2.1.1",
+ "esprima": "4.0.0",
+ "everything.js": "1.0.3",
+ "mocha": "5.0.0"
+ }
+ },
+ "node_modules/.pnpm/loose-envify@1.4.0": {
+ "extraneous": true
+ },
+ "node_modules/.pnpm/react-dom@18.2.0_react@18.2.0": {
+ "extraneous": true
+ },
+ "node_modules/.pnpm/react@18.2.0": {
+ "extraneous": true
+ },
+ "node_modules/.pnpm/scheduler@0.23.0": {
+ "extraneous": true
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.11",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
+ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.40",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.40.tgz",
+ "integrity": "sha512-H+BUhb9C1zBtogDLAk+KCNRKiHDrqSwQT/0z0PVTwMFBxqg3011ByLomADtgkgMkfwj4AMOiXBReyLTUBg681g==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
+ "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
+ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
+ "dev": true
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
+ "dev": true
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+ "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ }
+ }
+}
diff --git a/assets/package.json b/assets/package.json
index b423313..eb6593f 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -2,5 +2,10 @@
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.40",
+ "@types/react-dom": "^18.2.17",
+ "typescript": "^5.3.2"
}
}
diff --git a/assets/pnpm-lock.yaml b/assets/pnpm-lock.yaml
deleted file mode 100644
index 45f6600..0000000
--- a/assets/pnpm-lock.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-lockfileVersion: '6.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-dependencies:
- react:
- specifier: ^18.2.0
- version: 18.2.0
- react-dom:
- specifier: ^18.2.0
- version: 18.2.0(react@18.2.0)
-
-packages:
-
- /js-tokens@4.0.0:
- resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- dev: false
-
- /loose-envify@1.4.0:
- resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
- hasBin: true
- dependencies:
- js-tokens: 4.0.0
- dev: false
-
- /react-dom@18.2.0(react@18.2.0):
- resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
- peerDependencies:
- react: ^18.2.0
- dependencies:
- loose-envify: 1.4.0
- react: 18.2.0
- scheduler: 0.23.0
- dev: false
-
- /react@18.2.0:
- resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- loose-envify: 1.4.0
- dev: false
-
- /scheduler@0.23.0:
- resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
- dependencies:
- loose-envify: 1.4.0
- dev: false
diff --git a/assets/tsconfig.json b/assets/tsconfig.json
new file mode 100644
index 0000000..0b48eff
--- /dev/null
+++ b/assets/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ // https://esbuild.github.io/content-types/#tsconfig-json
+ "compilerOptions": {
+ // Keep in mind that ES6+ syntax to ES5 is not supported in esbuild yet.
+ "target": "es2016",
+ // Even when transpiling a single module, the TypeScript compiler actually
+ // parses imported files so it can tell whether an imported name is a type
+ // or a value. However, tools like esbuild compile each file in isolation so
+ // they can't tell if an imported name is a type or a value.
+ // https://esbuild.github.io/content-types/#isolated-modules
+ "isolatedModules": true,
+ // Disables legacy behavior around imports and makes TypeScript's type
+ // system compatible with ESM.
+ "esModuleInterop": true,
+ // Enables define semantics. In this mode, TypeScript class fields behave
+ // like normal JavaScript class fields. Field initializers do not trigger
+ // setters on the base class.
+ "useDefineForClassFields": true,
+ // If either of these options are enabled, esbuild will consider all code
+ // in all TypeScript files to be in strict mode and will prefix generated
+ // code with "use strict" unless the output format is set to esm (since all
+ // ESM files are automatically in strict mode).
+ "strict": true,
+ // Emit .js files with JSX changed to the equivalent React.createElement
+ // calls. It seems like the "react" value mirrors esbuild's native
+ // "transform" option, but it isn't obvious how these two relate from the
+ // documentation: https://esbuild.github.io/api/#jsx.
+ "jsx": "react"
+ }
+}
diff --git a/config/config.exs b/config/config.exs
index 161a6a1..4c20943 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -32,12 +32,23 @@ config :boardwise, BoardWiseWeb.Endpoint,
# at the `config/runtime.exs`.
config :boardwise, BoardWise.Mailer, adapter: Swoosh.Adapters.Local
-# Configure esbuild (the version is required)
+# Configure esbuild (the version is required). Aim to use the same target as
+# specified in tsconfig.json for ease of understanding. There are cases where
+# esbuild will interpret the tsconfig.json target independently of that
+# specified in this command (e.g. `useDefineForClassFields` as explained in
+# https://esbuild.github.io/content-types/#tsconfig-json).
config :esbuild,
version: "0.17.11",
default: [
- args:
- ~w(js/app.js js/react/main.jsx --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ args: ~w(
+ js/app.js
+ js/react/main.jsx
+ --bundle
+ --target=es2016
+ --outdir=../priv/static/assets
+ --external:/fonts/*
+ --external:/images/*
+ ),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
diff --git a/flake.nix b/flake.nix
index 08c6605..f3edcf4 100644
--- a/flake.nix
+++ b/flake.nix
@@ -44,8 +44,11 @@
] ++ (with pkgs; [
inotify-tools # For file watching in development.
mix2nix
- nodePackages.pnpm
+ nodePackages.prettier
+ nodePackages.typescript-language-server
+ nodejs
postgresql_15
+ typescript
]);
shellHook = ''
# The server will try to use the data directory named by this