diff --git a/config/vitest.ts b/config/vitest.ts new file mode 100644 index 00000000..92df5bf5 --- /dev/null +++ b/config/vitest.ts @@ -0,0 +1,16 @@ +// config/vitest.ts +// The vitest configuration file. + +import { env } from 'node:process'; +import { defineConfig } from 'vitest/config'; + +// Make sure the output of the CLI is in color, so that it matches the +// snapshots. +env.FORCE_COLOR = 2; + +export default defineConfig({ + test: { + // Collect coverage using C8. + coverage: { enabled: true }, + }, +}); diff --git a/package.json b/package.json index cff4d0e9..0e6f638c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "start": "node ./build/main.js", "compile": "tsup ./source/main.ts", "test:tsc": "tsc --project tsconfig.json", - "test": "pnpm test:tsc", + "test:unit": "vitest run --config config/vitest.ts", + "test:watch": "vitest watch --config config/vitest.ts", + "test": "pnpm test:tsc && pnpm test:unit", "lint:code": "eslint --max-warnings 0 source/**/*.ts", "lint:style": "prettier --check --ignore-path .gitignore .", "lint": "pnpm lint:code && pnpm lint:style", @@ -37,6 +39,7 @@ "ajv": "8.11.0", "arg": "5.0.2", "boxen": "7.0.0", + "c8": "7.11.3", "chalk": "5.0.1", "chalk-template": "0.4.0", "clipboardy": "3.0.0", @@ -50,12 +53,14 @@ "@types/serve-handler": "6.1.1", "@vercel/style-guide": "3.0.0", "eslint": "8.19.0", + "got": "12.1.0", "husky": "8.0.1", "lint-staged": "13.0.3", "prettier": "2.7.1", "tsup": "6.1.3", "tsx": "3.7.1", - "typescript": "4.6.4" + "typescript": "4.6.4", + "vitest": "0.18.0" }, "tsup": { "target": "esnext", @@ -79,7 +84,8 @@ "prettier --ignore-unknown --write" ], "source/**/*.ts": [ - "eslint --max-warnings 0 --fix" + "eslint --max-warnings 0 --fix", + "vitest related --run" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ce9110a..41a3ba08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,11 +8,13 @@ specifiers: ajv: 8.11.0 arg: 5.0.2 boxen: 7.0.0 + c8: 7.11.3 chalk: 5.0.1 chalk-template: 0.4.0 clipboardy: 3.0.0 compression: 1.7.4 eslint: 8.19.0 + got: 12.1.0 husky: 8.0.1 is-port-reachable: 4.0.0 lint-staged: 13.0.3 @@ -22,12 +24,14 @@ specifiers: tsx: 3.7.1 typescript: 4.6.4 update-check: 1.5.4 + vitest: 0.18.0 dependencies: '@zeit/schemas': 2.21.0 ajv: 8.11.0 arg: 5.0.2 boxen: 7.0.0 + c8: 7.11.3 chalk: 5.0.1 chalk-template: 0.4.0 clipboardy: 3.0.0 @@ -41,12 +45,14 @@ devDependencies: '@types/serve-handler': 6.1.1 '@vercel/style-guide': 3.0.0_rkmuuqx4yqfzhkhjmek6w3w2ju eslint: 8.19.0 + got: 12.1.0 husky: 8.0.1 lint-staged: 13.0.3 prettier: 2.7.1 tsup: 6.1.3_typescript@4.6.4 tsx: 3.7.1 typescript: 4.6.4 + vitest: 0.18.0_c8@7.11.3 packages: /@babel/code-frame/7.18.6: @@ -116,6 +122,12 @@ packages: regenerator-runtime: 0.13.9 dev: true + /@bcoe/v8-coverage/0.2.3: + resolution: + { + integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, + } + /@esbuild-kit/cjs-loader/2.3.0: resolution: { @@ -187,6 +199,35 @@ packages: } dev: true + /@istanbuljs/schema/0.1.3: + resolution: + { + integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==, + } + engines: { node: '>=8' } + + /@jridgewell/resolve-uri/3.1.0: + resolution: + { + integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==, + } + engines: { node: '>=6.0.0' } + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: + { + integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==, + } + + /@jridgewell/trace-mapping/0.3.14: + resolution: + { + integrity: sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==, + } + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + /@microsoft/tsdoc-config/0.15.2: resolution: { @@ -252,6 +293,24 @@ packages: } dev: true + /@sindresorhus/is/4.6.0: + resolution: + { + integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==, + } + engines: { node: '>=10' } + dev: true + + /@szmarczak/http-timer/5.0.1: + resolution: + { + integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==, + } + engines: { node: '>=14.16' } + dependencies: + defer-to-connect: 2.0.1 + dev: true + /@types/body-parser/1.19.2: resolution: { @@ -262,6 +321,34 @@ packages: '@types/node': 18.0.3 dev: true + /@types/cacheable-request/6.0.2: + resolution: + { + integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==, + } + dependencies: + '@types/http-cache-semantics': 4.0.1 + '@types/keyv': 3.1.4 + '@types/node': 18.0.3 + '@types/responselike': 1.0.0 + dev: true + + /@types/chai-subset/1.3.3: + resolution: + { + integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==, + } + dependencies: + '@types/chai': 4.3.1 + dev: true + + /@types/chai/4.3.1: + resolution: + { + integrity: sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==, + } + dev: true + /@types/compression/1.7.2: resolution: { @@ -303,6 +390,26 @@ packages: '@types/serve-static': 1.13.10 dev: true + /@types/http-cache-semantics/4.0.1: + resolution: + { + integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==, + } + dev: true + + /@types/istanbul-lib-coverage/2.0.4: + resolution: + { + integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==, + } + + /@types/json-buffer/3.0.0: + resolution: + { + integrity: sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==, + } + dev: true + /@types/json-schema/7.0.11: resolution: { @@ -317,6 +424,15 @@ packages: } dev: true + /@types/keyv/3.1.4: + resolution: + { + integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==, + } + dependencies: + '@types/node': 18.0.3 + dev: true + /@types/mime/1.3.2: resolution: { @@ -352,6 +468,15 @@ packages: } dev: true + /@types/responselike/1.0.0: + resolution: + { + integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==, + } + dependencies: + '@types/node': 18.0.3 + dev: true + /@types/serve-handler/6.1.1: resolution: { @@ -862,6 +987,13 @@ packages: es-shim-unscopables: 1.0.0 dev: true + /assertion-error/1.1.0: + resolution: + { + integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==, + } + dev: true + /ast-types-flow/0.0.7: resolution: { @@ -975,6 +1107,27 @@ packages: engines: { node: '>= 0.8' } dev: false + /c8/7.11.3: + resolution: + { + integrity: sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==, + } + engines: { node: '>=10.12.0' } + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.0 + istanbul-reports: 3.1.4 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.0.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + /cac/6.7.12: resolution: { @@ -983,6 +1136,30 @@ packages: engines: { node: '>=8' } dev: true + /cacheable-lookup/6.0.4: + resolution: + { + integrity: sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==, + } + engines: { node: '>=10.6.0' } + dev: true + + /cacheable-request/7.0.2: + resolution: + { + integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==, + } + engines: { node: '>=8' } + dependencies: + clone-response: 1.0.2 + get-stream: 5.2.0 + http-cache-semantics: 4.1.0 + keyv: 4.3.2 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.0 + dev: true + /call-bind/1.0.2: resolution: { @@ -1009,6 +1186,22 @@ packages: engines: { node: '>=14.16' } dev: false + /chai/4.3.6: + resolution: + { + integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==, + } + engines: { node: '>=4' } + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 3.0.1 + get-func-name: 2.0.0 + loupe: 2.3.4 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk-template/0.4.0: resolution: { @@ -1049,6 +1242,13 @@ packages: engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } dev: false + /check-error/1.0.2: + resolution: + { + integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==, + } + dev: true + /chokidar/3.5.3: resolution: { @@ -1144,6 +1344,25 @@ packages: is-wsl: 2.2.0 dev: false + /cliui/7.0.4: + resolution: + { + integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==, + } + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone-response/1.0.2: + resolution: + { + integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==, + } + dependencies: + mimic-response: 1.0.1 + dev: true + /color-convert/1.9.3: resolution: { @@ -1198,6 +1417,17 @@ packages: engines: { node: ^12.20.0 || >=14 } dev: true + /compress-brotli/1.3.8: + resolution: + { + integrity: sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==, + } + engines: { node: '>= 12' } + dependencies: + '@types/json-buffer': 3.0.0 + json-buffer: 3.0.1 + dev: true + /compressible/2.0.18: resolution: { @@ -1234,6 +1464,14 @@ packages: engines: { node: '>= 0.6' } dev: false + /convert-source-map/1.8.0: + resolution: + { + integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==, + } + dependencies: + safe-buffer: 5.1.2 + /core-js-pure/3.23.3: resolution: { @@ -1302,6 +1540,26 @@ packages: ms: 2.1.2 dev: true + /decompress-response/6.0.0: + resolution: + { + integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==, + } + engines: { node: '>=10' } + dependencies: + mimic-response: 3.1.0 + dev: true + + /deep-eql/3.0.1: + resolution: + { + integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==, + } + engines: { node: '>=0.12' } + dependencies: + type-detect: 4.0.8 + dev: true + /deep-extend/0.6.0: resolution: { @@ -1317,6 +1575,14 @@ packages: } dev: true + /defer-to-connect/2.0.1: + resolution: + { + integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==, + } + engines: { node: '>=10' } + dev: true + /define-properties/1.1.4: resolution: { @@ -1376,6 +1642,15 @@ packages: integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, } + /end-of-stream/1.4.4: + resolution: + { + integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, + } + dependencies: + once: 1.4.0 + dev: true + /error-ex/1.3.2: resolution: { @@ -1709,6 +1984,13 @@ packages: esbuild-windows-arm64: 0.14.48 dev: true + /escalade/3.1.1: + resolution: + { + integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==, + } + engines: { node: '>=6' } + /escape-string-regexp/1.0.5: resolution: { @@ -2280,6 +2562,16 @@ packages: path-exists: 4.0.0 dev: true + /find-up/5.0.0: + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: '>=10' } + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + /flat-cache/3.0.4: resolution: { @@ -2298,12 +2590,28 @@ packages: } dev: true + /foreground-child/2.0.0: + resolution: + { + integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==, + } + engines: { node: '>=8.0.0' } + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + + /form-data-encoder/1.7.1: + resolution: + { + integrity: sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==, + } + dev: true + /fs.realpath/1.0.0: resolution: { integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, } - dev: true /fsevents/2.3.2: resolution: @@ -2350,6 +2658,20 @@ packages: } dev: true + /get-caller-file/2.0.5: + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + + /get-func-name/2.0.0: + resolution: + { + integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==, + } + dev: true + /get-intrinsic/1.1.2: resolution: { @@ -2361,6 +2683,16 @@ packages: has-symbols: 1.0.3 dev: true + /get-stream/5.2.0: + resolution: + { + integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==, + } + engines: { node: '>=8' } + dependencies: + pump: 3.0.0 + dev: true + /get-stream/6.0.1: resolution: { @@ -2446,7 +2778,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /globals/13.16.0: resolution: @@ -2473,6 +2804,28 @@ packages: slash: 3.0.0 dev: true + /got/12.1.0: + resolution: + { + integrity: sha512-hBv2ty9QN2RdbJJMK3hesmSkFTjVIHyIDDbssCKnSmq62edGgImJWD10Eb1k77TiV1bxloxqcFAVK8+9pkhOig==, + } + engines: { node: '>=14.16' } + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 5.0.1 + '@types/cacheable-request': 6.0.2 + '@types/responselike': 1.0.0 + cacheable-lookup: 6.0.4 + cacheable-request: 7.0.2 + decompress-response: 6.0.0 + form-data-encoder: 1.7.1 + get-stream: 6.0.1 + http2-wrapper: 2.1.11 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 2.0.0 + dev: true + /has-bigints/1.0.2: resolution: { @@ -2539,6 +2892,30 @@ packages: } dev: true + /html-escaper/2.0.2: + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, + } + + /http-cache-semantics/4.1.0: + resolution: + { + integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==, + } + dev: true + + /http2-wrapper/2.1.11: + resolution: + { + integrity: sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==, + } + engines: { node: '>=10.19.0' } + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + /human-signals/2.1.0: resolution: { @@ -2606,14 +2983,12 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits/2.0.4: resolution: { integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, } - dev: true /ini/1.3.8: resolution: @@ -2864,6 +3239,34 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, } + /istanbul-lib-coverage/3.2.0: + resolution: + { + integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==, + } + engines: { node: '>=8' } + + /istanbul-lib-report/3.0.0: + resolution: + { + integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==, + } + engines: { node: '>=8' } + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 3.1.0 + supports-color: 7.2.0 + + /istanbul-reports/3.1.4: + resolution: + { + integrity: sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==, + } + engines: { node: '>=8' } + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.0 + /jju/1.4.0: resolution: { @@ -2896,6 +3299,13 @@ packages: argparse: 2.0.1 dev: true + /json-buffer/3.0.1: + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } + dev: true + /json-parse-even-better-errors/2.3.1: resolution: { @@ -2945,6 +3355,16 @@ packages: object.assign: 4.1.2 dev: true + /keyv/4.3.2: + resolution: + { + integrity: sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw==, + } + dependencies: + compress-brotli: 1.3.8 + json-buffer: 3.0.1 + dev: true + /language-subtag-registry/0.3.22: resolution: { @@ -3043,6 +3463,14 @@ packages: engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } dev: true + /local-pkg/0.4.2: + resolution: + { + integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==, + } + engines: { node: '>=14' } + dev: true + /locate-path/2.0.0: resolution: { @@ -3064,6 +3492,15 @@ packages: p-locate: 4.1.0 dev: true + /locate-path/6.0.0: + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: '>=10' } + dependencies: + p-locate: 5.0.0 + /lodash.merge/4.6.2: resolution: { @@ -3108,6 +3545,31 @@ packages: js-tokens: 4.0.0 dev: true + /loupe/2.3.4: + resolution: + { + integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==, + } + dependencies: + get-func-name: 2.0.0 + dev: true + + /lowercase-keys/2.0.0: + resolution: + { + integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==, + } + engines: { node: '>=8' } + dev: true + + /lowercase-keys/3.0.0: + resolution: + { + integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==, + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dev: true + /lru-cache/6.0.0: resolution: { @@ -3118,6 +3580,15 @@ packages: yallist: 4.0.0 dev: true + /make-dir/3.1.0: + resolution: + { + integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==, + } + engines: { node: '>=8' } + dependencies: + semver: 6.3.0 + /merge-stream/2.0.0: resolution: { @@ -3194,6 +3665,22 @@ packages: engines: { node: '>=12' } dev: true + /mimic-response/1.0.1: + resolution: + { + integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==, + } + engines: { node: '>=4' } + dev: true + + /mimic-response/3.1.0: + resolution: + { + integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==, + } + engines: { node: '>=10' } + dev: true + /min-indent/1.0.1: resolution: { @@ -3218,7 +3705,6 @@ packages: } dependencies: brace-expansion: 1.1.11 - dev: true /minimist/1.2.6: resolution: @@ -3257,6 +3743,15 @@ packages: thenify-all: 1.6.0 dev: true + /nanoid/3.3.4: + resolution: + { + integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + dev: true + /natural-compare/1.4.0: resolution: { @@ -3292,6 +3787,14 @@ packages: engines: { node: '>=0.10.0' } dev: true + /normalize-url/6.1.0: + resolution: + { + integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==, + } + engines: { node: '>=10' } + dev: true + /npm-run-path/4.0.1: resolution: { @@ -3408,7 +3911,6 @@ packages: } dependencies: wrappy: 1.0.2 - dev: true /onetime/5.1.2: resolution: @@ -3444,6 +3946,14 @@ packages: word-wrap: 1.2.3 dev: true + /p-cancelable/3.0.0: + resolution: + { + integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==, + } + engines: { node: '>=12.20' } + dev: true + /p-limit/1.3.0: resolution: { @@ -3464,6 +3974,15 @@ packages: p-try: 2.2.0 dev: true + /p-limit/3.1.0: + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: '>=10' } + dependencies: + yocto-queue: 0.1.0 + /p-locate/2.0.0: resolution: { @@ -3484,6 +4003,15 @@ packages: p-limit: 2.3.0 dev: true + /p-locate/5.0.0: + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: '>=10' } + dependencies: + p-limit: 3.1.0 + /p-map/4.0.0: resolution: { @@ -3547,7 +4075,6 @@ packages: integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, } engines: { node: '>=8' } - dev: true /path-is-absolute/1.0.1: resolution: @@ -3555,7 +4082,6 @@ packages: integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, } engines: { node: '>=0.10.0' } - dev: true /path-is-inside/1.0.2: resolution: { integrity: sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= } @@ -3598,6 +4124,20 @@ packages: engines: { node: '>=8' } dev: true + /pathval/1.1.1: + resolution: + { + integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==, + } + dev: true + + /picocolors/1.0.0: + resolution: + { + integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, + } + dev: true + /picomatch/2.3.1: resolution: { @@ -3650,6 +4190,18 @@ packages: yaml: 1.10.2 dev: true + /postcss/8.4.14: + resolution: + { + integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==, + } + engines: { node: ^10 || ^12 || >=14 } + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /prelude-ls/1.2.1: resolution: { @@ -3678,6 +4230,16 @@ packages: react-is: 16.13.1 dev: true + /pump/3.0.0: + resolution: + { + integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==, + } + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + /punycode/1.4.1: resolution: { @@ -3699,6 +4261,14 @@ packages: } dev: true + /quick-lru/5.1.1: + resolution: + { + integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==, + } + engines: { node: '>=10' } + dev: true + /range-parser/1.2.0: resolution: { @@ -3817,6 +4387,13 @@ packages: rc: 1.2.8 dev: false + /require-directory/2.1.1: + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: '>=0.10.0' } + /require-from-string/2.0.2: resolution: { @@ -3825,6 +4402,13 @@ packages: engines: { node: '>=0.10.0' } dev: false + /resolve-alpn/1.2.1: + resolution: + { + integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==, + } + dev: true + /resolve-from/4.0.0: resolution: { @@ -3875,6 +4459,15 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /responselike/2.0.0: + resolution: + { + integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==, + } + dependencies: + lowercase-keys: 2.0.0 + dev: true + /restore-cursor/3.1.0: resolution: { @@ -3909,7 +4502,6 @@ packages: hasBin: true dependencies: glob: 7.2.3 - dev: true /rollup/2.76.0: resolution: @@ -3945,7 +4537,6 @@ packages: { integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, } - dev: false /safe-buffer/5.2.1: resolution: @@ -3977,7 +4568,6 @@ packages: integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==, } hasBin: true - dev: true /semver/7.3.7: resolution: @@ -4082,6 +4672,14 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true + /source-map-js/1.0.2: + resolution: + { + integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, + } + engines: { node: '>=0.10.0' } + dev: true + /source-map-support/0.5.21: resolution: { @@ -4322,6 +4920,17 @@ packages: engines: { node: '>= 0.4' } dev: true + /test-exclude/6.0.0: + resolution: + { + integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, + } + engines: { node: '>=8' } + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + /text-table/0.2.0: resolution: { @@ -4355,6 +4964,22 @@ packages: } dev: true + /tinypool/0.2.2: + resolution: + { + integrity: sha512-tp4n5OARNL3v8ntdJUyo5NsDfwvUtu8isB43USjrsQxQrADDKY6UGBkmFaw/2vNmEt8S/uSm2U5FhkiK1eAFGw==, + } + engines: { node: '>=14.0.0' } + dev: true + + /tinyspy/0.3.3: + resolution: + { + integrity: sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==, + } + engines: { node: '>=14.0.0' } + dev: true + /to-regex-range/5.0.1: resolution: { @@ -4491,6 +5116,14 @@ packages: prelude-ls: 1.2.1 dev: true + /type-detect/4.0.8: + resolution: + { + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, + } + engines: { node: '>=4' } + dev: true + /type-fest/0.20.2: resolution: { @@ -4577,6 +5210,17 @@ packages: } dev: true + /v8-to-istanbul/9.0.1: + resolution: + { + integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==, + } + engines: { node: '>=10.12.0' } + dependencies: + '@jridgewell/trace-mapping': 0.3.14 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.8.0 + /validate-npm-package-license/3.0.4: resolution: { @@ -4595,6 +5239,75 @@ packages: engines: { node: '>= 0.8' } dev: false + /vite/2.9.14: + resolution: + { + integrity: sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==, + } + engines: { node: '>=12.2.0' } + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + dependencies: + esbuild: 0.14.48 + postcss: 8.4.14 + resolve: 1.22.1 + rollup: 2.76.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitest/0.18.0_c8@7.11.3: + resolution: + { + integrity: sha512-ryAtlh5Gvg3+aLNuOQ8YOHxgQCCu46jx40X5MBL0K0/ejB9i5zsr8fV8LTGXbXex80UMHlzceI9F+ouGaiR+mQ==, + } + engines: { node: '>=v14.16.0' } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/ui': '*' + c8: '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/ui': + optional: true + c8: + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.1 + '@types/chai-subset': 1.3.3 + '@types/node': 18.0.3 + c8: 7.11.3 + chai: 4.3.6 + debug: 4.3.4 + local-pkg: 0.4.2 + tinypool: 0.2.2 + tinyspy: 0.3.3 + vite: 2.9.14 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + dev: true + /webidl-conversions/4.0.2: resolution: { @@ -4676,7 +5389,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrap-ansi/8.0.1: resolution: @@ -4695,7 +5407,13 @@ packages: { integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, } - dev: true + + /y18n/5.0.8: + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: '>=10' } /yallist/4.0.0: resolution: @@ -4719,3 +5437,32 @@ packages: } engines: { node: '>= 14' } dev: true + + /yargs-parser/20.2.9: + resolution: + { + integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==, + } + engines: { node: '>=10' } + + /yargs/16.2.0: + resolution: + { + integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==, + } + engines: { node: '>=10' } + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + /yocto-queue/0.1.0: + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: '>=10' } diff --git a/source/main.ts b/source/main.ts index 1891a456..e629e5bb 100755 --- a/source/main.ts +++ b/source/main.ts @@ -3,88 +3,76 @@ // source/main.ts // The CLI for the `serve-handler` module. +import { cwd as getPwd, exit, env, stdout } from 'node:process'; import path from 'node:path'; import chalk from 'chalk'; import boxen from 'boxen'; import clipboard from 'clipboardy'; -import checkForUpdate from 'update-check'; import manifest from '../package.json'; import { resolve } from './utilities/promise.js'; import { startServer } from './utilities/server.js'; import { registerCloseListener } from './utilities/http.js'; -import { parseArguments, getHelpText } from './utilities/cli.js'; +import { + parseArguments, + getHelpText, + checkForUpdates, +} from './utilities/cli.js'; import { loadConfiguration } from './utilities/config.js'; import { logger } from './utilities/logger.js'; -import type { Arguments } from './types.js'; - -/** - * Checks for updates to this package. If an update is available, it brings it - * to the user's notice by printing a message to the console. - * - * @param debugMode - Whether or not we should print additional debug information. - * @returns - */ -const printUpdateNotification = async (debugMode?: boolean) => { - const [error, update] = await resolve(checkForUpdate(manifest)); - - if (error) { - const suffix = debugMode ? ':' : ' (use `--debug` to see full error).'; - logger.warn(`Checking for updates failed${suffix}`); - - if (debugMode) logger.error(error.message); - } - if (!update) return; - - logger.log( - chalk.bgRed.white(' UPDATE '), - `The latest version of \`serve\` is ${update.latest}.`, - ); -}; // Parse the options passed by the user. -let args: Arguments; -try { - args = parseArguments(); -} catch (error: unknown) { - logger.error((error as Error).message); - process.exit(1); +const [parseError, args] = await resolve(parseArguments()); +// Either TSC complains that `args` is undefined (which it shouldn't), or ESLint +// rightfully complains of an unnecessary condition. +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +if (parseError || !args) { + logger.error(parseError.message); + exit(1); } // Check for updates to the package unless the user sets the `NO_UPDATE_CHECK` // variable. -if (process.env.NO_UPDATE_CHECK !== '1') - await printUpdateNotification(args['--debug']); +const [updateError] = await resolve(checkForUpdates(manifest)); +if (updateError) { + const suffix = args['--debug'] ? ':' : ' (use `--debug` to see full error)'; + logger.warn(`Checking for updates failed${suffix}`); + + if (args['--debug']) logger.error(updateError.message); +} + // If the `version` or `help` arguments are passed, print the version or the // help text and exit. if (args['--version']) { logger.log(manifest.version); - process.exit(0); + exit(0); } if (args['--help']) { logger.log(getHelpText()); - process.exit(0); + exit(0); } // Default to listening on port 3000. if (!args['--listen']) - args['--listen'] = [ - [process.env.PORT ? parseInt(process.env.PORT, 10) : 3000], - ]; + args['--listen'] = [{ port: parseInt(env.PORT ?? '3000', 10) }]; // Ensure that the user has passed only one directory to serve. if (args._.length > 1) { logger.error('Please provide one path argument at maximum'); - process.exit(1); + exit(1); } -// Warn the user about using deprecated configuration files. -if (args['--config'] === 'now.json' || args['--config'] === 'package.json') - logger.warn( - 'The config files `now.json` and `package.json` are deprecated. Please use `serve.json`.', - ); // Parse the configuration. -const cwd = process.cwd(); -const entry = args._[0] ? path.resolve(args._[0]) : cwd; -const config = await loadConfiguration(cwd, entry, args); +const presentDirectory = getPwd(); +const directoryToServe = args._[0] ? path.resolve(args._[0]) : presentDirectory; +const [configError, config] = await resolve( + loadConfiguration(presentDirectory, directoryToServe, args), +); +// Either TSC complains that `args` is undefined (which it shouldn't), or ESLint +// rightfully complains of an unnecessary condition. +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +if (configError || !config) { + logger.error(configError.message); + exit(1); +} // If the user wants all the URLs rewritten to `/index.html`, make it happen. if (args['--single']) { @@ -115,8 +103,8 @@ for (const endpoint of args['--listen']) { // If we are not in a TTY or Node is running in production mode, print // a single line of text with the server address. - if (!process.stdout.isTTY || process.env.NODE_ENV === 'production') { - const suffix = local ? ` at ${local}.` : '.'; + if (!stdout.isTTY || env.NODE_ENV === 'production') { + const suffix = local ? ` at ${local}` : ''; logger.info(`Accepting connections${suffix}`); continue; @@ -170,6 +158,6 @@ registerCloseListener(() => { logger.log(); logger.warn('Force-closing all open sockets...'); - process.exit(0); + exit(0); }); }); diff --git a/source/types.ts b/source/types.ts index 7ae5cefa..c778b091 100644 --- a/source/types.ts +++ b/source/types.ts @@ -27,7 +27,10 @@ export declare type ListenEndpoint = | `pipe:\\\\.\\pipe\\${Host}`; // The parsed endpoints. -export declare type ParsedEndpoint = [Port] | [Host] | [Port, Host]; +export declare interface ParsedEndpoint { + port?: Port; + host?: Host; +} // An entry for URL rewrites. export declare interface Rewrite { diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index 11969066..6c7e6fa0 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -1,46 +1,18 @@ // source/utilities/cli.ts // CLI-related utility functions. -import chalk from 'chalk-template'; +import { parse as parseUrl } from 'node:url'; +import { env } from 'node:process'; +import chalk from 'chalk'; +import chalkTemplate from 'chalk-template'; import parseArgv from 'arg'; -import { parseEndpoint } from './http.js'; -import type { Arguments } from '../types.js'; - -// The options the CLI accepts, and how to parse them. -const options = { - '--help': Boolean, - '--version': Boolean, - '--listen': [parseEndpoint] as [typeof parseEndpoint], - '--single': Boolean, - '--debug': Boolean, - '--config': String, - '--no-clipboard': Boolean, - '--no-compression': Boolean, - '--no-etag': Boolean, - '--symlinks': Boolean, - '--cors': Boolean, - '--no-port-switching': Boolean, - '--ssl-cert': String, - '--ssl-key': String, - '--ssl-pass': String, - // A list of aliases for the above options. - '-h': '--help', - '-v': '--version', - '-l': '--listen', - '-s': '--single', - '-d': '--debug', - '-c': '--config', - '-n': '--no-clipboard', - '-u': '--no-compression', - '-S': '--symlinks', - '-C': '--cors', - - // The `-p` option is deprecated and is kept only for backwards-compatibility. - '-p': '--listen', -}; +import checkForUpdate from 'update-check'; +import { resolve } from './promise.js'; +import { logger } from './logger.js'; +import type { Arguments, ParsedEndpoint } from '../types.js'; // The help text for the CLI. -const helpText = chalk` +const helpText = chalkTemplate` {bold.cyan serve} - Static file serving and directory listing {bold USAGE} @@ -112,6 +84,93 @@ const helpText = chalk` {bold $} {cyan serve} -l pipe:\\\\.\\pipe\\{underline PipeName} `; +/** + * Returns the help text. + * + * @returns The help text shown when the `--help` option is used. + */ +export const getHelpText = (): string => helpText; + +/** + * Parse and return the endpoints from the given string. + * + * @param uriOrPort - The endpoint to listen on. + * @returns A list of parsed endpoints. + */ +export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => { + // If the endpoint is a port number, return it as is. + if (!isNaN(Number(uriOrPort))) return { port: Number(uriOrPort) }; + + // Cast it as a string, since we know for sure it is not a number. + const endpoint = uriOrPort; + + // We cannot use `new URL` here, otherwise it will not + // parse the host properly and it would drop support for IPv6. + const url = parseUrl(endpoint); + + switch (url.protocol) { + case 'pipe:': { + const pipe = endpoint.replace(/^pipe:/, ''); + if (!pipe.startsWith('\\\\.\\')) + throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`); + + return { host: pipe }; + } + case 'unix:': + if (!url.pathname) + throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`); + + return { host: url.pathname }; + case 'tcp:': + url.port = url.port ?? '3000'; + url.hostname = url.hostname ?? 'localhost'; + + return { + port: Number(url.port), + host: url.hostname, + }; + default: + throw new Error( + `Unknown --listen endpoint scheme (protocol): ${ + url.protocol ?? 'undefined' + }`, + ); + } +}; + +// The options the CLI accepts, and how to parse them. +const options = { + '--help': Boolean, + '--version': Boolean, + '--listen': [parseEndpoint] as [typeof parseEndpoint], + '--single': Boolean, + '--debug': Boolean, + '--config': String, + '--no-clipboard': Boolean, + '--no-compression': Boolean, + '--no-etag': Boolean, + '--symlinks': Boolean, + '--cors': Boolean, + '--no-port-switching': Boolean, + '--ssl-cert': String, + '--ssl-key': String, + '--ssl-pass': String, + // A list of aliases for the above options. + '-h': '--help', + '-v': '--version', + '-l': '--listen', + '-s': '--single', + '-d': '--debug', + '-c': '--config', + '-n': '--no-clipboard', + '-u': '--no-compression', + '-S': '--symlinks', + '-C': '--cors', + + // The `-p` option is deprecated and is kept only for backwards-compatibility. + '-p': '--listen', +}; + /** * Parses the program's `process.argv` and returns the options and arguments. * @@ -120,8 +179,23 @@ const helpText = chalk` export const parseArguments = (): Arguments => parseArgv(options); /** - * Returns the help text. - * - * @returns The help text shown when the `--help` option is used. + * Checks for updates to this package. If an update is available, it brings it + * to the user's notice by printing a message to the console. */ -export const getHelpText = (): string => helpText; +export const checkForUpdates = async (manifest: object): Promise => { + // Do not check for updates if the `NO_UPDATE_CHECK` variable is set. + if (env.NO_UPDATE_CHECK) return; + + // Check for a newer version of the package. + const [error, update] = await resolve(checkForUpdate(manifest)); + + // If there is an error, throw it; and if there is no update, return. + if (error) throw error; + if (!update) return; + + // If a newer version is available, tell the user. + logger.log( + chalk.bgRed.white(' UPDATE '), + `The latest version of \`serve\` is ${update.latest}`, + ); +}; diff --git a/source/utilities/config.ts b/source/utilities/config.ts index 75cd4a4c..db2e3c85 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -10,23 +10,24 @@ import Ajv from 'ajv'; // @ts-expect-error No type definitions. import schema from '@zeit/schemas/deployment/config-static.js'; import { resolve } from './promise.js'; +import { logger } from './logger.js'; import type { ErrorObject } from 'ajv'; import type { Configuration, Options, NodeError } from '../types.js'; /** * Parses and returns a configuration object from the designated locations. * - * @param cwd - The current working directory. - * @param entry - The directory to serve. + * @param presentDirectory - The current working directory. + * @param directoryToServe - The directory to serve. * @param args - The arguments passed to the CLI. * * @returns The parsed configuration. */ export const loadConfiguration = async ( - cwd: string, - entry: string, + presentDirectory: string, + directoryToServe: string, args: Partial, -): Promise => { +): Promise> => { const files = ['serve.json', 'now.json', 'package.json']; if (args['--config']) files.unshift(args['--config']); @@ -34,7 +35,7 @@ export const loadConfiguration = async ( for (const file of files) { // Resolve the path to the configuration file relative to the directory // with the content in it. - const location = resolvePath(entry, file); + const location = resolvePath(directoryToServe, file); // Disabling the lint rule as we don't want to read all the files at once; // if we can retrieve the configuration from the first file itself, we @@ -96,16 +97,25 @@ export const loadConfiguration = async ( // Once we have found a valid configuration, assign it and stop looking // through more configuration files. Object.assign(config, parsedJson); + + // Warn the user about using deprecated configuration files. + if (file === 'now.json' || file === 'package.json') + logger.warn( + 'The config files `now.json` and `package.json` are deprecated. Please use `serve.json`.', + ); + break; } - // Make sure the directory with the content is relative to the entry path + // Make sure the directory with the content is relative to the directoryToServe path // provided by the user. - if (entry) { + if (directoryToServe) { const staticDirectory = config.public; config.public = resolveRelativePath( - cwd, - staticDirectory ? resolvePath(entry, staticDirectory) : entry, + presentDirectory, + staticDirectory + ? resolvePath(directoryToServe, staticDirectory) + : directoryToServe, ); } @@ -130,5 +140,5 @@ export const loadConfiguration = async ( config.etag = !args['--no-etag']; config.symlinks = args['--symlinks'] || config.symlinks; - return config as Configuration; + return config; }; diff --git a/source/utilities/http.ts b/source/utilities/http.ts index e8019440..38bdb069 100644 --- a/source/utilities/http.ts +++ b/source/utilities/http.ts @@ -1,62 +1,16 @@ // source/utilities/http.ts // Helper functions for the server. -import { parse } from 'node:url'; import { networkInterfaces as getNetworkInterfaces } from 'node:os'; -import type { ParsedEndpoint } from '../types.js'; const networkInterfaces = getNetworkInterfaces(); -/** - * Parse and return the endpoints from the given string. - * - * @param uriOrPort - The endpoint to listen on. - * @returns A list of parsed endpoints. - */ -export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => { - // If the endpoint is a port number, return it as is. - if (!isNaN(Number(uriOrPort))) return [uriOrPort]; - - // Cast it as a string, since we know for sure it is not a number. - const endpoint = uriOrPort; - - // We cannot use `new URL` here, otherwise it will not - // parse the host properly and it would drop support for IPv6. - const url = parse(endpoint); - - switch (url.protocol) { - case 'pipe:': { - const pipe = endpoint.replace(/^pipe:/, ''); - if (!pipe.startsWith('\\\\.\\')) - throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`); - - return [pipe]; - } - case 'unix:': - if (!url.pathname) - throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`); - - return [url.pathname]; - case 'tcp:': - url.port = url.port ?? '3000'; - url.hostname = url.hostname ?? 'localhost'; - - return [parseInt(url.port, 10), url.hostname]; - default: - throw new Error( - `Unknown --listen endpoint scheme (protocol): ${ - url.protocol ?? 'undefined' - }`, - ); - } -}; - /** * Registers a function that runs on server shutdown. * * @param fn - The function to run on server shutdown */ -export const registerCloseListener = (fn: () => void) => { +export const registerCloseListener = (fn: () => void): void => { let run = false; const wrapper = () => { @@ -76,7 +30,7 @@ export const registerCloseListener = (fn: () => void) => { * * @returns The address of the host. */ -export const getNetworkAddress = () => { +export const getNetworkAddress = (): string | undefined => { for (const interfaceDetails of Object.values(networkInterfaces)) { if (!interfaceDetails) continue; diff --git a/source/utilities/logger.ts b/source/utilities/logger.ts index dec9114c..dad688a2 100644 --- a/source/utilities/logger.ts +++ b/source/utilities/logger.ts @@ -6,11 +6,11 @@ import chalk from 'chalk'; const info = (...message: string[]) => - console.error(chalk.magenta('INFO:', ...message)); + console.error(chalk.bgMagenta.bold(' INFO '), ...message); const warn = (...message: string[]) => - console.error(chalk.yellow('WARNING:', ...message)); + console.error(chalk.bgYellow.bold(' WARN '), ...message); const error = (...message: string[]) => - console.error(chalk.red('ERROR:', ...message)); + console.error(chalk.bgRed.bold(' ERROR '), ...message); const log = console.log; export const logger = { info, warn, error, log }; diff --git a/source/utilities/promise.ts b/source/utilities/promise.ts index 12645175..a090ccb6 100644 --- a/source/utilities/promise.ts +++ b/source/utilities/promise.ts @@ -13,16 +13,20 @@ * else console.log(data) * ``` * - * @param promise - The promise to resolve. + * @param promiseLike - The promise to resolve. * @returns An array containing the error as the first element, and the resolved * data as the second element. */ -export const resolve = ( - promise: Promise, -): Promise<[E, undefined] | [undefined, T]> => - promise - .then<[undefined, T]>((data) => [undefined, data]) - .catch<[E, undefined]>((error) => [error, undefined]); +export const resolve = async ( + promiseLike: Promise | T, +): Promise<[E, undefined] | [undefined, T]> => { + try { + const data = await promiseLike; + return [undefined, data]; + } catch (error: unknown) { + return [error as E, undefined]; + } +}; /** * Promisifies the passed function. diff --git a/source/utilities/server.ts b/source/utilities/server.ts index 360389c1..44d2e63e 100644 --- a/source/utilities/server.ts +++ b/source/utilities/server.ts @@ -122,17 +122,17 @@ export const startServer = async ( // If the endpoint is a non-zero port, make sure it is not occupied. if ( - typeof endpoint[0] === 'number' && - !isNaN(endpoint[0]) && - endpoint[0] !== 0 + typeof endpoint.port === 'number' && + !isNaN(endpoint.port) && + endpoint.port !== 0 ) { - const port = endpoint[0]; + const port = endpoint.port; const isClosed = await isPortReachable(port, { - host: endpoint[1] ?? 'localhost', + host: endpoint.host ?? 'localhost', }); // If the port is already taken, then start the server on a random port // instead. - if (isClosed) return startServer([0], config, args, port); + if (isClosed) return startServer({ port: 0 }, config, args, port); // Otherwise continue on to starting the server. } @@ -140,18 +140,23 @@ export const startServer = async ( // Finally, start the server. return new Promise((resolve, _reject) => { // If only a port is specified, listen on the given port on localhost. - if (endpoint.length === 1 && typeof endpoint[0] === 'number') - server.listen(endpoint[0], () => resolve(getServerDetails())); + if ( + typeof endpoint.port !== 'undefined' && + typeof endpoint.host === 'undefined' + ) + server.listen(endpoint.port, () => resolve(getServerDetails())); // If the path to a socket or a pipe is given, listen on it. - else if (endpoint.length === 1 && typeof endpoint[0] === 'string') - server.listen(endpoint[0], () => resolve(getServerDetails())); + else if ( + typeof endpoint.port === 'undefined' && + typeof endpoint.host !== 'undefined' + ) + server.listen(endpoint.host, () => resolve(getServerDetails())); // If a port number and hostname are given, listen on `host:port`. else if ( - endpoint.length === 2 && - typeof endpoint[0] === 'number' && - typeof endpoint[1] === 'string' + typeof endpoint.port !== 'undefined' && + typeof endpoint.host !== 'undefined' ) - server.listen(endpoint[0], endpoint[1], () => + server.listen(endpoint.port, endpoint.host, () => resolve(getServerDetails()), ); }); diff --git a/tests/__fixtures__/config/custom/app/index.html b/tests/__fixtures__/config/custom/app/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/config/custom/app/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/config/custom/config.json b/tests/__fixtures__/config/custom/config.json new file mode 100644 index 00000000..5df3e5ec --- /dev/null +++ b/tests/__fixtures__/config/custom/config.json @@ -0,0 +1,4 @@ +{ + "public": "app/", + "renderSingle": true +} diff --git a/tests/__fixtures__/config/deprecated/app/index.html b/tests/__fixtures__/config/deprecated/app/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/config/deprecated/app/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/config/deprecated/package.json b/tests/__fixtures__/config/deprecated/package.json new file mode 100644 index 00000000..df56a95f --- /dev/null +++ b/tests/__fixtures__/config/deprecated/package.json @@ -0,0 +1,5 @@ +{ + "static": { + "public": "app/" + } +} diff --git a/tests/__fixtures__/config/invalid/app/index.html b/tests/__fixtures__/config/invalid/app/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/config/invalid/app/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/config/invalid/serve.json b/tests/__fixtures__/config/invalid/serve.json new file mode 100644 index 00000000..ab021223 --- /dev/null +++ b/tests/__fixtures__/config/invalid/serve.json @@ -0,0 +1,3 @@ +{ + "symlink": ["app/"] +} diff --git a/tests/__fixtures__/config/non-existent/index.html b/tests/__fixtures__/config/non-existent/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/config/non-existent/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/config/valid/app/index.html b/tests/__fixtures__/config/valid/app/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/config/valid/app/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/config/valid/serve.json b/tests/__fixtures__/config/valid/serve.json new file mode 100644 index 00000000..5df3e5ec --- /dev/null +++ b/tests/__fixtures__/config/valid/serve.json @@ -0,0 +1,4 @@ +{ + "public": "app/", + "renderSingle": true +} diff --git a/tests/__fixtures__/server/app/index.html b/tests/__fixtures__/server/app/index.html new file mode 100644 index 00000000..061fca55 --- /dev/null +++ b/tests/__fixtures__/server/app/index.html @@ -0,0 +1,23 @@ + + + + + + + + Serve Application + + + + + + + + + + Hello there! + + diff --git a/tests/__fixtures__/server/serve.json b/tests/__fixtures__/server/serve.json new file mode 100644 index 00000000..5df3e5ec --- /dev/null +++ b/tests/__fixtures__/server/serve.json @@ -0,0 +1,4 @@ +{ + "public": "app/", + "renderSingle": true +} diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap new file mode 100644 index 00000000..e7a18109 --- /dev/null +++ b/tests/__snapshots__/cli.test.ts.snap @@ -0,0 +1,75 @@ +// Vitest Snapshot v1 + +exports[`utilities/cli > render help text 1`] = ` +" + serve - Static file serving and directory listing + + USAGE + + $ serve --help + $ serve --version + $ serve folder_name + $ serve [-l listen_uri [-l ...]] [directory] + + By default, serve will listen on 0.0.0.0:3000 and serve the + current working directory on that address. + + Specifying a single --listen argument will overwrite the default, not supplement it. + + OPTIONS + + --help Shows this help message + + -v, --version Displays the current version of serve + + -l, --listen listen_uri Specify a URI endpoint on which to listen (see below) - + more than one may be specified to listen in multiple places + + -p Specify custom port + + -d, --debug Show debugging information + + -s, --single Rewrite all not-found requests to \`index.html\` + + -c, --config Specify custom path to \`serve.json\` + + -C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\` + + -n, --no-clipboard Do not copy the local address to the clipboard + + -u, --no-compression Do not compress files + + --no-etag Send \`Last-Modified\` header instead of \`ETag\` + + -S, --symlinks Resolve symlinks instead of showing 404 errors + + --ssl-cert Optional path to an SSL/TLS certificate to serve with HTTPS + + --ssl-key Optional path to the SSL/TLS certificate's private key + + --ssl-pass Optional path to the SSL/TLS certificate's passphrase + + --no-port-switching Do not open a port other than the one specified when it's taken. + + ENDPOINTS + + Listen endpoints (specified by the --listen or -l options above) instruct serve + to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes. + + For TCP ports on hostname \\"localhost\\": + + $ serve -l 1234 + + For TCP (traditional host/port) endpoints: + + $ serve -l tcp://hostname:1234 + + For UNIX domain socket endpoints: + + $ serve -l unix:/path/to/socket.sock + + For Windows named pipe endpoints: + + $ serve -l pipe:\\\\\\\\.\\\\pipe\\\\PipeName +" +`; diff --git a/tests/__snapshots__/config.test.ts.snap b/tests/__snapshots__/config.test.ts.snap new file mode 100644 index 00000000..d1972f53 --- /dev/null +++ b/tests/__snapshots__/config.test.ts.snap @@ -0,0 +1,35 @@ +// Vitest Snapshot v1 + +exports[`utilities/config > parse valid config 1`] = ` +{ + "etag": true, + "public": "tests/__fixtures__/config/valid/app", + "renderSingle": true, + "symlinks": undefined, +} +`; + +exports[`utilities/config > parse valid config at custom location 1`] = ` +{ + "etag": true, + "public": "tests/__fixtures__/config/custom/app", + "renderSingle": true, + "symlinks": undefined, +} +`; + +exports[`utilities/config > return default configuration when no source is found 1`] = ` +{ + "etag": true, + "public": "tests/__fixtures__/config/non-existent", + "symlinks": undefined, +} +`; + +exports[`utilities/config > warn when configuration comes from a deprecated source 1`] = ` +{ + "etag": true, + "public": "tests/__fixtures__/config/deprecated/app", + "symlinks": undefined, +} +`; diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 00000000..21ba189a --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,103 @@ +// tests/cli.test.ts +// Tests for the CLI part of the project. + +import { env } from 'node:process'; +import { afterEach, describe, test, expect, vi } from 'vitest'; + +import manifest from '../package.json'; +import { + getHelpText, + parseEndpoint, + checkForUpdates, +} from '../source/utilities/cli.js'; +import { logger } from '../source/utilities/logger.js'; +import { ParsedEndpoint } from '../source/types.js'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +// A list of cases used to test the `parseEndpoint` function. The first element +// is the name of the case, followed by the input to pass to the function, +// followed by the expected output. +type EndpointTestCase = [string, string, ParsedEndpoint]; +const validEndpoints = [ + ['http port', '4242', { port: 4242 }], + ['tcp url', 'tcp://localhost:4242', { port: 4242, host: 'localhost' }], + ['unix socket', 'unix:///dev/sock1', { host: '/dev/sock1' }], + ['pipe', 'pipe:\\\\.\\pipe\\localhost', { host: '\\\\.\\pipe\\localhost' }], +] as EndpointTestCase[]; +// Another list of cases used to test the `parseEndpoint` function. The function +// should throw an error when parsing any of these cases, as they are invalid +// endpoints. +type InvalidEndpointTestCase = [string, string, RegExp]; +const invalidEndpoints = [ + ['protocol', 'ws://localhost', /unknown.*endpoint.*scheme.*/i], + ['unix socket', 'unix://', /invalid.*unix.*socket.*/i], + ['windows pipe', 'pipe:\\localhost', /invalid.*pipe.*/i], +] as InvalidEndpointTestCase[]; + +describe('utilities/cli', () => { + // Make sure the help message remains the same. If we are changing the help + // message, then make sure to run `vitest` with the `--update-snapshot` flag. + test('render help text', () => expect(getHelpText()).toMatchSnapshot()); + + // Make sure the `parseEndpoint` function parses valid endpoints correctly. + test.each(validEndpoints)( + 'parse %s as endpoint', + (_name, endpoint, parsedEndpoint) => + expect(parseEndpoint(endpoint)).toEqual(parsedEndpoint), + ); + + // Make sure `parseEndpoint` throws errors on invalid endpoints. + test.each(invalidEndpoints)( + 'parse %s as endpoint', + (_name, endpoint, error) => + expect(() => parseEndpoint(endpoint)).toThrow(error), + ); + + // Make sure the update message is shown when the current version is not + // the latest version. + test('print update message when newer version exists', async () => { + const consoleSpy = vi.spyOn(logger, 'log'); + + await checkForUpdates({ + ...manifest, + version: '0.0.0', + }); + + expect(consoleSpy).toHaveBeenCalledOnce(); + expect(consoleSpy).toHaveBeenLastCalledWith( + expect.stringContaining('UPDATE'), + expect.stringContaining('latest'), + ); + }); + + // Make sure the update message is not shown when the latest version is + // running. + test('do not print update message when on latest version', async () => { + const consoleSpy = vi.spyOn(logger, 'log'); + + await checkForUpdates({ + ...manifest, + version: '99.99.99', + }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + // Make sure an update check does not occur when the NO_UPDATE_CHECK env var + // is set. + test('do not check for updates when NO_UPDATE_CHECK is set', async () => { + const consoleSpy = vi.spyOn(logger, 'log'); + + env.NO_UPDATE_CHECK = 'true'; + await checkForUpdates({ + ...manifest, + version: '0.0.0', + }); + env.NO_UPDATE_CHECK = undefined; + + expect(consoleSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 00000000..3240d1f0 --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,68 @@ +// tests/config.test.ts +// Tests for the configuration loader. + +import { afterEach, describe, test, expect, vi } from 'vitest'; + +import { loadConfiguration } from '../source/utilities/config.js'; +import { logger } from '../source/utilities/logger.js'; +import { Options } from '../source/types.js'; + +// The path to the fixtures for this test file. +const fixtures = 'tests/__fixtures__/config/'; +// A helper function to load the configuration for a certain fixture. +const loadConfig = ( + name: 'valid' | 'invalid' | 'non-existent' | 'deprecated', + args?: Partial = {}, +) => loadConfiguration(process.cwd(), `${fixtures}/${name}`, args); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('utilities/config', () => { + // Make sure the configuration is parsed correctly when it is in the + // `serve.json` file. + test('parse valid config', async () => { + const configuration = await loadConfig('valid'); + expect(configuration).toMatchSnapshot(); + }); + + // Make sure the configuration is parsed correctly when it is a location + // specified by the `--config` option. + test('parse valid config at custom location', async () => { + const configuration = await loadConfig('custom', { + '--config': 'config.json', + }); + expect(configuration).toMatchSnapshot(); + }); + + // When the configuration in the file is invalid, the function will throw an + // error. + test('throw error if config is invalid', async () => { + loadConfig('invalid').catch((error: Error) => { + expect(error.message).toMatch(/invalid/); + }); + }); + + // When no configuration file exists, the configuration should be populated + // with the `etag` and `symlink` options set to their default values, and + // the `public` option set to the path of the directory. + test('return default configuration when no source is found', async () => { + const configuration = await loadConfig('non-existent'); + expect(configuration).toMatchSnapshot(); + }); + + // When the configuration source is deprecated, i.e., the configuration lives + // in `now.json` or `package.json`, a warning should be printed. + test('warn when configuration comes from a deprecated source', async () => { + const consoleSpy = vi.spyOn(logger, 'warn'); + + const configuration = await loadConfig('deprecated'); + expect(configuration).toMatchSnapshot(); + + expect(consoleSpy).toHaveBeenCalledOnce(); + expect(consoleSpy).toHaveBeenLastCalledWith( + expect.stringContaining('deprecated'), + ); + }); +}); diff --git a/tests/server.test.ts b/tests/server.test.ts new file mode 100644 index 00000000..151ba541 --- /dev/null +++ b/tests/server.test.ts @@ -0,0 +1,57 @@ +// tests/config.test.ts +// Tests for the configuration loader. + +import { afterEach, describe, test, expect, vi } from 'vitest'; +import { extend as createFetch } from 'got'; + +import { loadConfiguration } from '../source/utilities/config.js'; +import { startServer } from '../source/utilities/server.js'; + +// The path to the fixtures for this test file. +const fixture = 'tests/__fixtures__/server/'; +// The configuration from the fixture. +const config = await loadConfiguration(process.cwd(), fixture, {}); +// A `fetch` instance to make requests to the server. +const fetch = createFetch({ throwHttpErrors: false }); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('utilities/server', () => { + // Make sure the server starts on the specified port. + test('start server on specified port', async () => { + const address = await startServer({ port: 3001 }, config, {}); + + expect(address.local).toBe('http://localhost:3001'); + expect(address.network).toMatch(/^http:\/\/.*:3001$/); + expect(address.previous).toBeUndefined(); + + const response = await fetch(address.local!); + expect(response.ok); + }); + + // Make sure the server starts on the specified port and host. + test('start server on specified port and host', async () => { + const address = await startServer({ port: 3002, host: '::1' }, config, {}); + + expect(address.local).toBe('http://[::1]:3002'); + expect(address.network).toMatch(/^http:\/\/.*:3002$/); + expect(address.previous).toBeUndefined(); + + const response = await fetch(address.local!); + expect(response.ok); + }); + + // Make sure the server starts on the specified port and host. + test('start server on different port if port is already occupied', async () => { + const address = await startServer({ port: 3002, host: '::1' }, config, {}); + + expect(address.local).not.toBe('http://[::1]:3002'); + expect(address.network).not.toMatch(/^http:\/\/.*:3002$/); + expect(address.previous).toBe(3002); + + const response = await fetch(address.local!); + expect(response.ok); + }); +});