diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6cfb678 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0895156 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Test + run: npm run test:ci diff --git a/.gitignore b/.gitignore index 45df8b7..ef8291e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules /dist -/app \ No newline at end of file +/app +/coverage \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 4d23e4c..a563846 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "useTabs": false, "trailingComma": "all", "bracketSpacing": true, - "singleQuote": true + "singleQuote": true, + "semi": true } diff --git a/package-lock.json b/package-lock.json index 58d196f..bb9f977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,75 @@ "typescript": "^5.4.4" }, "devDependencies": { + "@vitest/coverage-v8": "^1.4.0", "prettier": "^3.2.5", "tsup": "^8.0.2", "vitest": "^1.4.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -404,6 +468,15 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -729,6 +802,12 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", @@ -740,6 +819,34 @@ "undici-types": "~5.26.4" } }, + "node_modules/@vitest/coverage-v8": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz", + "integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.4.0" + } + }, "node_modules/@vitest/expect": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz", @@ -1085,6 +1192,18 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1309,6 +1428,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1386,6 +1511,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -1404,6 +1544,22 @@ "node": ">= 4" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1470,6 +1626,56 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", + "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -1806,6 +2012,32 @@ "node": ">=12" } }, + "node_modules/magicast": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", + "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "source-map-js": "^1.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1955,6 +2187,15 @@ "node": ">=0.10.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -1975,6 +2216,15 @@ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2287,6 +2537,33 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2501,6 +2778,74 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2546,6 +2891,15 @@ "node": ">=14.0.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3080,6 +3434,20 @@ "optional": true, "peer": true }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vite": { "version": "5.2.8", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", @@ -3523,6 +3891,18 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yaml": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", diff --git a/package.json b/package.json index cebcc17..d5fa469 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ }, "sideEffects": false, "scripts": { - "build": "tsup" + "build": "tsup", + "test": "vitest", + "test:ci": "vitest run --coverage --reporter=basic" }, "author": "Jon Ambas ", "license": "MIT", @@ -34,6 +36,7 @@ "typescript": "^5.4.4" }, "devDependencies": { + "@vitest/coverage-v8": "^1.4.0", "prettier": "^3.2.5", "tsup": "^8.0.2", "vitest": "^1.4.0" diff --git a/src/__tests__/get.test.ts b/src/__tests__/get.test.ts new file mode 100644 index 0000000..9fb3a7b --- /dev/null +++ b/src/__tests__/get.test.ts @@ -0,0 +1,72 @@ +import { get, getTemplate } from '../get'; + +const tokens = { + foo: { a: { b: { c: { value: { base: '10px', lg: '20px' } } } } }, + bar: { baz: { 100: { value: {} }, 200: { value: {} } } }, + baz: { 100: 'hello', 200: { value: 'goodbye' } }, +}; + +describe('get', () => { + it('gets a string', () => { + expect(get(tokens)('baz.100')).toBe('"hello"'); + }); + + it('gets a value object', () => { + expect(get(tokens)('foo.a.b.c')).toMatchInlineSnapshot( + `"{"base":"10px","lg":"20px"}"`, + ); + }); + + it('gets a value string', () => { + expect(get(tokens)('baz.200')).toMatchInlineSnapshot(`""goodbye""`); + }); + + it('gets an undefined token', () => { + expect(get(tokens)('nope.nope')).toBe(`"panda-plugin-ct-alias-not-found"`); + }); + + it('gets an undefined path', () => { + // @ts-expect-error Checking arg omission + expect(get(tokens)()).toBe(`"panda-plugin-ct-alias-not-found"`); + }); +}); + +describe('getTemplate', () => { + it('generates a ct function', () => { + expect(getTemplate({ foo: { value: { base: '#fff', md: '#000' } } })) + .toMatchInlineSnapshot(` + " + const pluginCtTokens = { + "foo": { + "value": { + "base": "#fff", + "md": "#000" + } + } + }; + + export const ct = (path) => { + if (!path) return "panda-plugin-ct-alias-not-found"; + + const parts = path.split('.'); + let current = pluginCtTokens; + + for (const part of parts) { + if (!current[part]) break; + current = current[part]; + } + + if (typeof current === 'string') { + return current; + } + + if (typeof current === 'object' && current != null && !Array.isArray(current) && 'value' in current) { + return current.value; + } + + return "panda-plugin-ct-alias-not-found"; + }; + " + `); + }); +}); diff --git a/src/__tests__/tsconfig.json b/src/__tests__/tsconfig.json new file mode 100644 index 0000000..4a57ad7 --- /dev/null +++ b/src/__tests__/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "types": ["vitest/globals"] + }, + "include": ["./**/*.ts"] +} diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts new file mode 100644 index 0000000..7f7599c --- /dev/null +++ b/src/__tests__/utils.test.ts @@ -0,0 +1,49 @@ +import { isObject, makePaths } from '../utils'; + +describe('isObject', () => { + it('returns true if an object', () => { + expect(isObject({})).toBe(true); + expect(isObject({ foo: 'bar' })).toBe(true); + }); + + it('returns false if not an object', () => { + expect(isObject(1)).toBe(false); + expect(isObject('1')).toBe(false); + expect(isObject(undefined)).toBe(false); + expect(isObject(null)).toBe(false); + expect(isObject([1, 2, 3])).toBe(false); + }); +}); + +describe('makePaths', () => { + it('makes paths', () => { + expect( + makePaths({ + foo: { 100: { value: '#fff' }, 200: '' }, + bar: { 100: '', 200: '' }, + }), + ).toMatchInlineSnapshot(` + [ + "foo.100", + "foo.200", + "bar.100", + "bar.200", + ] + `); + }); + + it('makes paths with object values', () => { + expect( + makePaths({ + foo: { a: { b: { c: { value: { base: '', lg: '' } } } } }, + bar: { baz: { 100: { value: '#fff' }, 200: { value: {} } } }, + }), + ).toMatchInlineSnapshot(` + [ + "foo.a.b.c", + "bar.baz.100", + "bar.baz.200", + ] + `); + }); +}); diff --git a/src/codegen.ts b/src/codegen.ts index 5c5cccf..90113bb 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -5,6 +5,7 @@ import type { } from '@pandacss/types'; import { makePaths } from './utils'; import type { PluginContext } from './types'; +import { getTemplate } from './get'; export const codegen = ( args: CodegenPrepareHookArgs, @@ -19,26 +20,7 @@ export const codegen = ( const cssFile = cssFn.files.find((f) => f.file.includes('css.mjs')); if (!cssFile) return args.artifacts; - cssFile.code += `\n - const ctTokens = ${JSON.stringify(tokens, null, 2)}; - - export const ct = (path) => { - const parts = path.split("."); - let current = ctTokens; - - for (const part of parts) { - if (!current[part]) { - break; - } - current = current[part]; - } - - if (typeof current !== "string") { - return "panda-plugin-ct-alias-not-found"; - } - - return current; - }`; + cssFile.code += getTemplate(tokens); const cssDtsFile = cssFn.files.find((f) => f.file.includes('css.d.')); if (!cssDtsFile) return args.artifacts; diff --git a/src/get.ts b/src/get.ts new file mode 100644 index 0000000..fe2fe0d --- /dev/null +++ b/src/get.ts @@ -0,0 +1,56 @@ +import type { ComponentTokens } from './types'; +import { isObject } from './utils'; + +const missing = `"panda-plugin-ct-alias-not-found"`; + +export const get = + (tokens: ComponentTokens) => + (path: string): string => { + if (!path) return missing; + + const parts = path.split('.'); + let current = tokens; + + for (const part of parts) { + if (!current[part]) break; + current = current[part] as ComponentTokens; + } + + if (typeof current === 'string') { + return `"${current}"`; + } + + if (isObject(current) && 'value' in current) { + return typeof current.value === 'string' + ? `"${current.value}"` + : JSON.stringify(current.value); + } + + return missing; + }; + +export const getTemplate = (tokens: ComponentTokens) => ` +const pluginCtTokens = ${JSON.stringify(tokens, null, 2)}; + +export const ct = (path) => { + if (!path) return ${missing}; + + const parts = path.split('.'); + let current = pluginCtTokens; + + for (const part of parts) { + if (!current[part]) break; + current = current[part]; + } + + if (typeof current === 'string') { + return current; + } + + if (typeof current === 'object' && current != null && !Array.isArray(current) && 'value' in current) { + return current.value; + } + + return ${missing}; +}; +`; diff --git a/src/parser.ts b/src/parser.ts index 7365537..ae9df4c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,5 +1,6 @@ import type { ParserResultBeforeHookArgs } from '@pandacss/types'; -import type { ComponentTokens, PluginContext } from './types'; +import type { PluginContext } from './types'; +import { get } from './get'; export const parser = ( args: ParserResultBeforeHookArgs, @@ -20,32 +21,16 @@ export const parser = ( const text = source.getText(); const calls = text.match(/ct\(['"][\w.]+['"]\)/g) ?? []; + const ct = get(tokens); let newText = text; - const get = (path: string): string => { - const parts = path.split('.'); - let current = tokens; - - for (const part of parts) { - if (!current[part]) break; - current = current[part] as ComponentTokens; - } - - // TODO: allow passing through style objects - if (typeof current !== 'string') { - return 'panda-plugin-ct-alias-not-found'; - } - - return current as unknown as string; - }; - for (const call of calls) { const path = call .match(/['"][\w.]+['"]/) ?.toString() .replace(/['"]/g, ''); if (!path) continue; - newText = newText.replace(call, `"${get(path)}"`); + newText = newText.replace(call, ct(path)); } return newText; diff --git a/src/utils.ts b/src/utils.ts index d61fa57..650c222 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,15 +6,16 @@ export const makePaths = ( obj: Record, prefix?: string, ): string[] => { - const keys = Object.keys(obj); const pathPrefix = prefix ? prefix + '.' : ''; + const paths = []; - return keys.reduce((acc, key) => { - if (isObject(obj[key])) { - acc = acc.concat(makePaths(obj[key], pathPrefix + key)); + for (const [key, value] of Object.entries(obj)) { + if (!isObject(value) || 'value' in value) { + paths.push(`${pathPrefix}${key}`); } else { - acc.push(pathPrefix + key); + paths.push(...makePaths(value, `${pathPrefix}${key}`)); } - return acc; - }, []); + } + + return paths; }; diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..de71548 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + coverage: { + include: ['src', '!src/**/__tests__/**'], + }, + }, +});