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