From 7ea6b20b099ccbc1c55546634e84af5cdc1eafbd Mon Sep 17 00:00:00 2001 From: Vedant K Date: Wed, 13 Jul 2022 10:10:19 +0530 Subject: [PATCH 1/9] tests(unit): add cli unit tests --- package.json | 10 +- pnpm-lock.yaml | 487 ++++++++++++++++++++++++++- source/utilities/cli.ts | 126 ++++--- source/utilities/http.ts | 46 --- tests/__snapshots__/cli.test.ts.snap | 75 +++++ tests/cli.test.ts | 36 ++ 6 files changed, 672 insertions(+), 108 deletions(-) create mode 100644 tests/__snapshots__/cli.test.ts.snap create mode 100644 tests/cli.test.ts diff --git a/package.json b/package.json index 2736b124..e71173f7 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "start": "node ./build/main.js", "compile": "tsup ./source/main.ts", "test:tsc": "tsc --project tsconfig.json", - "test": "pnpm test:tsc", + "test:unit": "vitest run --coverage", + "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 +38,7 @@ "ajv": "8.11.0", "arg": "5.0.2", "boxen": "7.0.0", + "c8": "7.11.3", "chalk": "5.0.1", "clipboardy": "3.0.0", "compression": "1.7.4", @@ -54,7 +56,8 @@ "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", @@ -78,7 +81,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 829a873a..a62437e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ specifiers: ajv: 8.11.0 arg: 5.0.2 boxen: 7.0.0 + c8: 7.11.3 chalk: 5.0.1 clipboardy: 3.0.0 compression: 1.7.4 @@ -21,12 +22,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 clipboardy: 3.0.0 compression: 1.7.4 @@ -45,6 +48,7 @@ devDependencies: 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: @@ -114,6 +118,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: { @@ -185,6 +195,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: { @@ -260,6 +299,22 @@ packages: '@types/node': 18.0.3 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: { @@ -301,6 +356,12 @@ packages: '@types/serve-static': 1.13.10 dev: true + /@types/istanbul-lib-coverage/2.0.4: + resolution: + { + integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==, + } + /@types/json-schema/7.0.11: resolution: { @@ -754,7 +815,6 @@ packages: engines: { node: '>=8' } dependencies: color-convert: 2.0.1 - dev: true /ansi-styles/6.1.0: resolution: @@ -861,6 +921,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: { @@ -974,6 +1041,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: { @@ -1008,6 +1096,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/2.4.1: resolution: { @@ -1039,6 +1143,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: { @@ -1134,6 +1245,16 @@ 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 + /color-convert/1.9.3: resolution: { @@ -1151,7 +1272,6 @@ packages: engines: { node: '>=7.0.0' } dependencies: color-name: 1.1.4 - dev: true /color-name/1.1.3: resolution: @@ -1165,7 +1285,6 @@ packages: { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, } - dev: true /colorette/2.0.19: resolution: @@ -1226,6 +1345,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: { @@ -1294,6 +1421,16 @@ packages: ms: 2.1.2 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: { @@ -1701,6 +1838,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: { @@ -2272,6 +2416,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: { @@ -2290,12 +2444,21 @@ 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 + /fs.realpath/1.0.0: resolution: { integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, } - dev: true /fsevents/2.3.2: resolution: @@ -2342,6 +2505,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: { @@ -2438,7 +2615,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /globals/13.16.0: resolution: @@ -2486,7 +2662,6 @@ packages: integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, } engines: { node: '>=8' } - dev: true /has-property-descriptors/1.0.0: resolution: @@ -2532,6 +2707,12 @@ packages: } dev: true + /html-escaper/2.0.2: + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, + } + /human-signals/2.1.0: resolution: { @@ -2599,14 +2780,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: @@ -2857,6 +3036,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: { @@ -3036,6 +3243,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: { @@ -3057,6 +3272,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: { @@ -3101,6 +3325,15 @@ 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 + /lru-cache/6.0.0: resolution: { @@ -3111,6 +3344,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: { @@ -3211,7 +3453,6 @@ packages: } dependencies: brace-expansion: 1.1.11 - dev: true /minimist/1.2.6: resolution: @@ -3250,6 +3491,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: { @@ -3401,7 +3651,6 @@ packages: } dependencies: wrappy: 1.0.2 - dev: true /onetime/5.1.2: resolution: @@ -3457,6 +3706,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: { @@ -3477,6 +3735,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: { @@ -3540,7 +3807,6 @@ packages: integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, } engines: { node: '>=8' } - dev: true /path-is-absolute/1.0.1: resolution: @@ -3548,7 +3814,6 @@ packages: integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, } engines: { node: '>=0.10.0' } - dev: true /path-is-inside/1.0.2: resolution: { integrity: sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= } @@ -3591,6 +3856,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: { @@ -3643,6 +3922,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: { @@ -3810,6 +4101,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: { @@ -3902,7 +4200,6 @@ packages: hasBin: true dependencies: glob: 7.2.3 - dev: true /rollup/2.76.0: resolution: @@ -3938,7 +4235,6 @@ packages: { integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, } - dev: false /safe-buffer/5.2.1: resolution: @@ -3970,7 +4266,6 @@ packages: integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==, } hasBin: true - dev: true /semver/7.3.7: resolution: @@ -4075,6 +4370,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: { @@ -4306,7 +4609,6 @@ packages: engines: { node: '>=8' } dependencies: has-flag: 4.0.0 - dev: true /supports-preserve-symlinks-flag/1.0.0: resolution: @@ -4316,6 +4618,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: { @@ -4349,6 +4662,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: { @@ -4485,6 +4814,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: { @@ -4571,6 +4908,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: { @@ -4589,6 +4937,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: { @@ -4670,7 +5087,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: @@ -4689,7 +5105,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: @@ -4713,3 +5135,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/utilities/cli.ts b/source/utilities/cli.ts index f32aa8e0..c3966d01 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -1,43 +1,10 @@ // source/utilities/cli.ts // CLI-related utility functions. +import { parse as parseUrl } from 'node:url'; import chalk from 'chalk'; 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 type { Arguments, ParsedEndpoint } from '../types.js'; // The help text for the CLI. const helpText = chalk` @@ -113,15 +80,92 @@ const helpText = chalk` `; /** - * Parses the program's `process.argv` and returns the options and arguments. + * Returns the help text. * - * @returns The parsed options and arguments. + * @returns The help text shown when the `--help` option is used. */ -export const parseArguments = (): Arguments => parseArgv(options); +export const getHelpText = (): string => helpText; /** - * Returns the help text. + * Parse and return the endpoints from the given string. * - * @returns The help text shown when the `--help` option is used. + * @param uriOrPort - The endpoint to listen on. + * @returns A list of parsed endpoints. */ -export const getHelpText = (): string => helpText; +export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => { + // If the endpoint is a port number, return it as is. + if (!isNaN(Number(uriOrPort))) return [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 [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 [Number(url.port), 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. + * + * @returns The parsed options and arguments. + */ +export const parseArguments = (): Arguments => parseArgv(options); diff --git a/source/utilities/http.ts b/source/utilities/http.ts index e8019440..a9225de4 100644 --- a/source/utilities/http.ts +++ b/source/utilities/http.ts @@ -1,56 +1,10 @@ // 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. * diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap new file mode 100644 index 00000000..caafb215 --- /dev/null +++ b/tests/__snapshots__/cli.test.ts.snap @@ -0,0 +1,75 @@ +// Vitest Snapshot v1 + +exports[`utilities/cli > render help text 1`] = ` +" + {bold.cyan serve} - Static file serving and directory listing + + {bold USAGE} + + {bold $} {cyan serve} --help + {bold $} {cyan serve} --version + {bold $} {cyan serve} folder_name + {bold $} {cyan serve} [-l {underline listen_uri} [-l ...]] [{underline directory}] + + By default, {cyan serve} will listen on {bold 0.0.0.0:3000} and serve the + current working directory on that address. + + Specifying a single {bold --listen} argument will overwrite the default, not supplement it. + + {bold OPTIONS} + + --help Shows this help message + + -v, --version Displays the current version of serve + + -l, --listen {underline 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. + + {bold ENDPOINTS} + + Listen endpoints (specified by the {bold --listen} or {bold -l} options above) instruct {cyan serve} + to listen on one or more interfaces/ports, UNIX domain sockets, or Windows named pipes. + + For TCP ports on hostname \\"localhost\\": + + {bold $} {cyan serve} -l {underline 1234} + + For TCP (traditional host/port) endpoints: + + {bold $} {cyan serve} -l tcp://{underline hostname}:{underline 1234} + + For UNIX domain socket endpoints: + + {bold $} {cyan serve} -l unix:{underline /path/to/socket.sock} + + For Windows named pipe endpoints: + + {bold $} {cyan serve} -l pipe:\\\\\\\\.\\\\pipe\\\\{underline PipeName} +" +`; diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 00000000..1a03551d --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,36 @@ +// tests/cli.test.ts +// Tests for the CLI part of the project. + +import { describe, test, expect } from 'vitest'; + +import { getHelpText, parseEndpoint } from '../source/utilities/cli.js'; +import { ParsedEndpoint } from '../source/types.js'; + +type EndpointTestCase = [string, string, ParsedEndpoint]; +const validEndpoints = [ + ['http port', '4242', [4242]], + ['tcp url', 'tcp://localhost:4242', [4242, 'localhost']], + ['unix socket', 'unix:///dev/sock1', ['/dev/sock1']], + ['pipe', 'pipe:\\\\.\\pipe\\localhost', ['\\\\.\\pipe\\localhost']], +] as EndpointTestCase[]; + +describe('utilities/cli', () => { + test('render help text', () => expect(getHelpText()).toMatchSnapshot()); + + test.each(validEndpoints)( + 'parse %s as endpoint', + (_name, endpoint, parsedEndpoint) => + expect(parseEndpoint(endpoint)).toEqual(parsedEndpoint), + ); + + test('detect invalid unix socket', () => + expect(() => parseEndpoint('unix://')).toThrow(/invalid.*unix.*/i)); + test('detect invalid windows pipe', () => + expect(() => parseEndpoint('pipe:\\localhost')).toThrow( + /invalid.*pipe.*/i, + )); + test('detect invalid endpoint protocol', () => + expect(() => parseEndpoint('ws://localhost')).toThrow( + /unknown.*endpoint.*scheme.*/i, + )); +}); From 31160b4518a19a7da18f634ae8c4ee3548538556 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Wed, 13 Jul 2022 17:00:12 +0530 Subject: [PATCH 2/9] tests(unit): add config unit tests --- config/vitest.ts | 11 +++ package.json | 3 +- source/main.ts | 87 ++++++++----------- source/utilities/cli.ts | 27 ++++++ source/utilities/config.ts | 8 ++ source/utilities/logger.ts | 6 +- source/utilities/promise.ts | 18 ++-- .../config/deprecated/app/index.html | 23 +++++ .../config/deprecated/package.json | 5 ++ .../config/invalid/app/index.html | 23 +++++ tests/__fixtures__/config/invalid/serve.json | 3 + .../config/non-existent/index.html | 23 +++++ .../__fixtures__/config/valid/app/index.html | 23 +++++ tests/__fixtures__/config/valid/serve.json | 4 + tests/__snapshots__/config.test.ts.snap | 26 ++++++ tests/cli.test.ts | 31 ++++--- tests/config.test.ts | 57 ++++++++++++ 17 files changed, 308 insertions(+), 70 deletions(-) create mode 100644 config/vitest.ts create mode 100644 tests/__fixtures__/config/deprecated/app/index.html create mode 100644 tests/__fixtures__/config/deprecated/package.json create mode 100644 tests/__fixtures__/config/invalid/app/index.html create mode 100644 tests/__fixtures__/config/invalid/serve.json create mode 100644 tests/__fixtures__/config/non-existent/index.html create mode 100644 tests/__fixtures__/config/valid/app/index.html create mode 100644 tests/__fixtures__/config/valid/serve.json create mode 100644 tests/__snapshots__/config.test.ts.snap create mode 100644 tests/config.test.ts diff --git a/config/vitest.ts b/config/vitest.ts new file mode 100644 index 00000000..fd7aa0f3 --- /dev/null +++ b/config/vitest.ts @@ -0,0 +1,11 @@ +// config/vitest.ts +// The vitest configuration file. + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Collect coverage using C8. + coverage: { enabled: true }, + }, +}); diff --git a/package.json b/package.json index e71173f7..cf82583f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "start": "node ./build/main.js", "compile": "tsup ./source/main.ts", "test:tsc": "tsc --project tsconfig.json", - "test:unit": "vitest run --coverage", + "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 .", diff --git a/source/main.ts b/source/main.ts index e3afb265..7a6b184d 100755 --- a/source/main.ts +++ b/source/main.ts @@ -3,87 +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()); +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'] = [[env.PORT ? parseInt(env.PORT, 10) : 3000]]; // 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 cwd = getPwd(); const entry = args._[0] ? path.resolve(args._[0]) : cwd; -const config = await loadConfiguration(cwd, entry, args); +const [configError, config] = await resolve( + loadConfiguration(cwd, entry, 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']) { @@ -114,7 +103,7 @@ 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') { + if (!stdout.isTTY || env.NODE_ENV === 'production') { const suffix = local ? ` at ${local}` : ''; logger.info(`Accepting connections${suffix}`); @@ -167,6 +156,6 @@ registerCloseListener(() => { logger.log(); logger.warn('Force-closing all open sockets...'); - process.exit(0); + exit(0); }); }); diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index c3966d01..4c1c2bce 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -2,8 +2,13 @@ // CLI-related utility functions. import { parse as parseUrl } from 'node:url'; +import { env } from 'node:process'; import chalk from 'chalk'; import parseArgv from 'arg'; +import checkForUpdate from 'update-check'; +import manifest from '../../package.json'; +import { resolve } from './promise.js'; +import { logger } from './logger.js'; import type { Arguments, ParsedEndpoint } from '../types.js'; // The help text for the CLI. @@ -169,3 +174,25 @@ const options = { * @returns The parsed options and arguments. */ export const parseArguments = (): Arguments => parseArgv(options); + +/** + * 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 checkForUpdates = async () => { + // 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..e3bdd008 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -10,6 +10,7 @@ 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'; @@ -96,6 +97,13 @@ 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; } 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/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/__snapshots__/config.test.ts.snap b/tests/__snapshots__/config.test.ts.snap new file mode 100644 index 00000000..ff6274b1 --- /dev/null +++ b/tests/__snapshots__/config.test.ts.snap @@ -0,0 +1,26 @@ +// 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 > 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 index 1a03551d..dfd0969a 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -6,6 +6,9 @@ import { describe, test, expect } from 'vitest'; import { getHelpText, parseEndpoint } from '../source/utilities/cli.js'; import { ParsedEndpoint } from '../source/types.js'; +// 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', [4242]], @@ -13,24 +16,32 @@ const validEndpoints = [ ['unix socket', 'unix:///dev/sock1', ['/dev/sock1']], ['pipe', 'pipe:\\\\.\\pipe\\localhost', ['\\\\.\\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), ); - test('detect invalid unix socket', () => - expect(() => parseEndpoint('unix://')).toThrow(/invalid.*unix.*/i)); - test('detect invalid windows pipe', () => - expect(() => parseEndpoint('pipe:\\localhost')).toThrow( - /invalid.*pipe.*/i, - )); - test('detect invalid endpoint protocol', () => - expect(() => parseEndpoint('ws://localhost')).toThrow( - /unknown.*endpoint.*scheme.*/i, - )); + // Make sure `parseEndpoint` throws errors on invalid endpoints. + test.each(invalidEndpoints)( + 'parse %s as endpoint', + (_name, endpoint, error) => + expect(() => parseEndpoint(endpoint)).toThrow(error), + ); }); diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 00000000..e611e940 --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,57 @@ +// 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'; + +// 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', +) => loadConfiguration(process.cwd(), `${fixtures}/${name}`, {}); + +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(); + }); + + // When the configuration in the file is invalid, the function will throw an + // error. + test('parse invalid config', 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'), + ); + }); +}); From 6046c4fffbb6fb2b82c29f40e34fc7c365fc9662 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Thu, 14 Jul 2022 13:12:06 +0530 Subject: [PATCH 3/9] test: add tests for update checker and custom config files --- source/main.ts | 2 +- source/utilities/cli.ts | 3 +- source/utilities/config.ts | 4 +- source/utilities/http.ts | 4 +- .../__fixtures__/config/custom/app/index.html | 23 +++++++ tests/__fixtures__/config/custom/config.json | 4 ++ tests/__snapshots__/config.test.ts.snap | 9 +++ tests/cli.test.ts | 60 ++++++++++++++++++- tests/config.test.ts | 15 ++++- 9 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 tests/__fixtures__/config/custom/app/index.html create mode 100644 tests/__fixtures__/config/custom/config.json diff --git a/source/main.ts b/source/main.ts index 7a6b184d..6b8b22cd 100755 --- a/source/main.ts +++ b/source/main.ts @@ -32,7 +32,7 @@ if (parseError || !args) { // Check for updates to the package unless the user sets the `NO_UPDATE_CHECK` // variable. -const [updateError] = await resolve(checkForUpdates()); +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}`); diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index 4c1c2bce..a5cb0d54 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -6,7 +6,6 @@ import { env } from 'node:process'; import chalk from 'chalk'; import parseArgv from 'arg'; import checkForUpdate from 'update-check'; -import manifest from '../../package.json'; import { resolve } from './promise.js'; import { logger } from './logger.js'; import type { Arguments, ParsedEndpoint } from '../types.js'; @@ -179,7 +178,7 @@ export const parseArguments = (): Arguments => parseArgv(options); * 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 checkForUpdates = async () => { +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; diff --git a/source/utilities/config.ts b/source/utilities/config.ts index e3bdd008..e1dbc3aa 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -27,7 +27,7 @@ export const loadConfiguration = async ( cwd: string, entry: string, args: Partial, -): Promise => { +): Promise> => { const files = ['serve.json', 'now.json', 'package.json']; if (args['--config']) files.unshift(args['--config']); @@ -138,5 +138,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 a9225de4..38bdb069 100644 --- a/source/utilities/http.ts +++ b/source/utilities/http.ts @@ -10,7 +10,7 @@ const networkInterfaces = getNetworkInterfaces(); * * @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 = () => { @@ -30,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/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/__snapshots__/config.test.ts.snap b/tests/__snapshots__/config.test.ts.snap index ff6274b1..d1972f53 100644 --- a/tests/__snapshots__/config.test.ts.snap +++ b/tests/__snapshots__/config.test.ts.snap @@ -9,6 +9,15 @@ exports[`utilities/config > parse valid config 1`] = ` } `; +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, diff --git a/tests/cli.test.ts b/tests/cli.test.ts index dfd0969a..2ffe10d9 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -1,11 +1,22 @@ // tests/cli.test.ts // Tests for the CLI part of the project. -import { describe, test, expect } from 'vitest'; +import { env } from 'node:process'; +import { afterEach, describe, test, expect, vi } from 'vitest'; -import { getHelpText, parseEndpoint } from '../source/utilities/cli.js'; +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. @@ -44,4 +55,49 @@ describe('utilities/cli', () => { (_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 index e611e940..3240d1f0 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -5,13 +5,15 @@ 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', -) => loadConfiguration(process.cwd(), `${fixtures}/${name}`, {}); + args?: Partial = {}, +) => loadConfiguration(process.cwd(), `${fixtures}/${name}`, args); afterEach(() => { vi.restoreAllMocks(); @@ -25,9 +27,18 @@ describe('utilities/config', () => { 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('parse invalid config', async () => { + test('throw error if config is invalid', async () => { loadConfig('invalid').catch((error: Error) => { expect(error.message).toMatch(/invalid/); }); From 26a89779b00297c46b5af9c94a5db3ad06a446b7 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Thu, 14 Jul 2022 13:24:12 +0530 Subject: [PATCH 4/9] chore: merge commits from `upstream/main` --- package.json | 3 ++- pnpm-lock.yaml | 13 ++++++++++- source/main.ts | 4 +++- source/utilities/cli.ts | 3 ++- tests/__snapshots__/cli.test.ts.snap | 32 ++++++++++++++-------------- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index cf82583f..aa09f9ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serve", - "version": "14.0.0", + "version": "14.0.1", "description": "Static file serving and directory listing", "keywords": [ "vercel", @@ -41,6 +41,7 @@ "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", "is-port-reachable": "4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a62437e7..ce97e295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,7 @@ specifiers: 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 @@ -31,6 +32,7 @@ dependencies: 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 is-port-reachable: 4.0.0 @@ -1112,6 +1114,16 @@ packages: type-detect: 4.0.8 dev: true + /chalk-template/0.4.0: + resolution: + { + integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==, + } + engines: { node: '>=12' } + dependencies: + chalk: 4.1.2 + dev: false + /chalk/2.4.1: resolution: { @@ -1133,7 +1145,6 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true /chalk/5.0.1: resolution: diff --git a/source/main.ts b/source/main.ts index 6b8b22cd..d619f427 100755 --- a/source/main.ts +++ b/source/main.ts @@ -133,7 +133,9 @@ for (const endpoint of args['--listen']) { await clipboard.write(local); message += `\n\n${chalk.grey('Copied local address to clipboard!')}`; } catch (error: unknown) { - logger.error(`Cannot copy to clipboard: ${(error as Error).message}`); + logger.error( + `Cannot copy server address to clipboard: ${(error as Error).message}.`, + ); } } diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index a5cb0d54..d9df901d 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -4,6 +4,7 @@ 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 checkForUpdate from 'update-check'; import { resolve } from './promise.js'; @@ -11,7 +12,7 @@ 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} diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index caafb215..e7a18109 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -2,27 +2,27 @@ exports[`utilities/cli > render help text 1`] = ` " - {bold.cyan serve} - Static file serving and directory listing + serve - Static file serving and directory listing - {bold USAGE} + USAGE - {bold $} {cyan serve} --help - {bold $} {cyan serve} --version - {bold $} {cyan serve} folder_name - {bold $} {cyan serve} [-l {underline listen_uri} [-l ...]] [{underline directory}] + $ serve --help + $ serve --version + $ serve folder_name + $ serve [-l listen_uri [-l ...]] [directory] - By default, {cyan serve} will listen on {bold 0.0.0.0:3000} and serve the + By default, serve will listen on 0.0.0.0:3000 and serve the current working directory on that address. - Specifying a single {bold --listen} argument will overwrite the default, not supplement it. + Specifying a single --listen argument will overwrite the default, not supplement it. - {bold OPTIONS} + OPTIONS --help Shows this help message -v, --version Displays the current version of serve - -l, --listen {underline listen_uri} Specify a URI endpoint on which to listen (see below) - + -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 @@ -51,25 +51,25 @@ exports[`utilities/cli > render help text 1`] = ` --no-port-switching Do not open a port other than the one specified when it's taken. - {bold ENDPOINTS} + ENDPOINTS - Listen endpoints (specified by the {bold --listen} or {bold -l} options above) instruct {cyan serve} + 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\\": - {bold $} {cyan serve} -l {underline 1234} + $ serve -l 1234 For TCP (traditional host/port) endpoints: - {bold $} {cyan serve} -l tcp://{underline hostname}:{underline 1234} + $ serve -l tcp://hostname:1234 For UNIX domain socket endpoints: - {bold $} {cyan serve} -l unix:{underline /path/to/socket.sock} + $ serve -l unix:/path/to/socket.sock For Windows named pipe endpoints: - {bold $} {cyan serve} -l pipe:\\\\\\\\.\\\\pipe\\\\{underline PipeName} + $ serve -l pipe:\\\\\\\\.\\\\pipe\\\\PipeName " `; From ff28a2b691ab08b740b00bf05ef0f9080eb1d934 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Thu, 14 Jul 2022 17:36:13 +0530 Subject: [PATCH 5/9] refc(types): make parsed endpoint an object --- source/main.ts | 8 ++++---- source/types.ts | 5 ++++- source/utilities/cli.ts | 11 +++++++---- source/utilities/config.ts | 20 +++++++++++--------- source/utilities/server.ts | 28 ++++++++++++---------------- tests/cli.test.ts | 10 +++++----- 6 files changed, 43 insertions(+), 39 deletions(-) diff --git a/source/main.ts b/source/main.ts index d619f427..e629e5bb 100755 --- a/source/main.ts +++ b/source/main.ts @@ -53,7 +53,7 @@ if (args['--help']) { // Default to listening on port 3000. if (!args['--listen']) - args['--listen'] = [[env.PORT ? parseInt(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'); @@ -61,10 +61,10 @@ if (args._.length > 1) { } // Parse the configuration. -const cwd = getPwd(); -const entry = args._[0] ? path.resolve(args._[0]) : cwd; +const presentDirectory = getPwd(); +const directoryToServe = args._[0] ? path.resolve(args._[0]) : presentDirectory; const [configError, config] = await resolve( - loadConfiguration(cwd, entry, args), + loadConfiguration(presentDirectory, directoryToServe, args), ); // Either TSC complains that `args` is undefined (which it shouldn't), or ESLint // rightfully complains of an unnecessary condition. 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 d9df901d..6c7e6fa0 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -99,7 +99,7 @@ export const getHelpText = (): string => helpText; */ export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => { // If the endpoint is a port number, return it as is. - if (!isNaN(Number(uriOrPort))) return [Number(uriOrPort)]; + 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; @@ -114,18 +114,21 @@ export const parseEndpoint = (uriOrPort: string): ParsedEndpoint => { if (!pipe.startsWith('\\\\.\\')) throw new Error(`Invalid Windows named pipe endpoint: ${endpoint}`); - return [pipe]; + return { host: pipe }; } case 'unix:': if (!url.pathname) throw new Error(`Invalid UNIX domain socket endpoint: ${endpoint}`); - return [url.pathname]; + return { host: url.pathname }; case 'tcp:': url.port = url.port ?? '3000'; url.hostname = url.hostname ?? 'localhost'; - return [Number(url.port), url.hostname]; + return { + port: Number(url.port), + host: url.hostname, + }; default: throw new Error( `Unknown --listen endpoint scheme (protocol): ${ diff --git a/source/utilities/config.ts b/source/utilities/config.ts index e1dbc3aa..db2e3c85 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -17,15 +17,15 @@ 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> => { const files = ['serve.json', 'now.json', 'package.json']; @@ -35,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 @@ -107,13 +107,15 @@ export const loadConfiguration = async ( 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, ); } diff --git a/source/utilities/server.ts b/source/utilities/server.ts index 360389c1..61293128 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,14 @@ 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 (endpoint.port && !endpoint.host) + 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 (endpoint.host && !endpoint.port) + server.listen(endpoint.port, () => 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' - ) - server.listen(endpoint[0], endpoint[1], () => + else if (endpoint.port && endpoint.host) + server.listen(endpoint.port, endpoint.host, () => resolve(getServerDetails()), ); }); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 2ffe10d9..21ba189a 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -22,10 +22,10 @@ afterEach(() => { // followed by the expected output. type EndpointTestCase = [string, string, ParsedEndpoint]; const validEndpoints = [ - ['http port', '4242', [4242]], - ['tcp url', 'tcp://localhost:4242', [4242, 'localhost']], - ['unix socket', 'unix:///dev/sock1', ['/dev/sock1']], - ['pipe', 'pipe:\\\\.\\pipe\\localhost', ['\\\\.\\pipe\\localhost']], + ['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 @@ -91,7 +91,7 @@ describe('utilities/cli', () => { test('do not check for updates when NO_UPDATE_CHECK is set', async () => { const consoleSpy = vi.spyOn(logger, 'log'); - env.NO_UPDATE_CHECK = true; + env.NO_UPDATE_CHECK = 'true'; await checkForUpdates({ ...manifest, version: '0.0.0', From e4c9187aa0b057097bd08fd3924f8581949a43df Mon Sep 17 00:00:00 2001 From: Vedant K Date: Fri, 15 Jul 2022 13:20:58 +0530 Subject: [PATCH 6/9] test: add tests for `utilities/server` --- package.json | 1 + pnpm-lock.yaml | 291 +++++++++++++++++++++++ tests/__fixtures__/server/app/index.html | 23 ++ tests/__fixtures__/server/serve.json | 4 + tests/server.test.ts | 57 +++++ 5 files changed, 376 insertions(+) create mode 100644 tests/__fixtures__/server/app/index.html create mode 100644 tests/__fixtures__/server/serve.json create mode 100644 tests/server.test.ts diff --git a/package.json b/package.json index aa09f9ef..0e6f638c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce97e295..41a3ba08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ specifiers: 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 @@ -44,6 +45,7 @@ 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 @@ -291,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: { @@ -301,6 +321,18 @@ 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: { @@ -358,12 +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: { @@ -378,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: { @@ -413,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: { @@ -1072,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: { @@ -1266,6 +1354,15 @@ packages: 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: { @@ -1320,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: { @@ -1432,6 +1540,16 @@ 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: { @@ -1457,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: { @@ -1516,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: { @@ -2465,6 +2600,13 @@ packages: 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: { @@ -2541,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: { @@ -2652,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: { @@ -2724,6 +2898,24 @@ packages: 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: { @@ -3107,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: { @@ -3156,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: { @@ -3345,6 +3554,22 @@ packages: 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: { @@ -3440,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: { @@ -3546,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: { @@ -3697,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: { @@ -3973,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: { @@ -3994,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: { @@ -4127,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: { @@ -4177,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: { 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/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); + }); +}); From ffcd4c90b5cf2f8c50a6af5cdc4818caca751e10 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Fri, 15 Jul 2022 15:34:48 +0530 Subject: [PATCH 7/9] test: force color output for tests --- config/vitest.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/vitest.ts b/config/vitest.ts index fd7aa0f3..92df5bf5 100644 --- a/config/vitest.ts +++ b/config/vitest.ts @@ -1,8 +1,13 @@ // 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. From 3522dcd02e62e76cc71fd9adfaef6c90ef8a5e87 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Sat, 16 Jul 2022 09:50:00 +0530 Subject: [PATCH 8/9] fix(utilities/server): check if port and host are `undefined`, not falsy --- source/utilities/server.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/source/utilities/server.ts b/source/utilities/server.ts index 61293128..44d2e63e 100644 --- a/source/utilities/server.ts +++ b/source/utilities/server.ts @@ -140,13 +140,22 @@ 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.port && !endpoint.host) + 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.host && !endpoint.port) - server.listen(endpoint.port, () => 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.port && endpoint.host) + else if ( + typeof endpoint.port !== 'undefined' && + typeof endpoint.host !== 'undefined' + ) server.listen(endpoint.port, endpoint.host, () => resolve(getServerDetails()), ); From 8fbc0fd175b6a4f9ecc62073343b8aeab2309e05 Mon Sep 17 00:00:00 2001 From: Vedant K Date: Mon, 18 Jul 2022 15:36:46 +0530 Subject: [PATCH 9/9] chore: merge commits from `upstream/main` --- readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index fd751ec4..e8febc13 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,8 @@ ## Usage +> `serve` v14 onwards requires Node v14 to run. Please use `serve` v13 if you cannot upgrade to Node v14. + The quickest way to get started is to just run `npx serve` in your project's directory. If you prefer, you can also install the package globally (you'll need at least [Node LTS](https://github.com/nodejs/Release#release-schedule)): @@ -60,7 +62,7 @@ To customize `serve`'s behavior, create a `serve.json` file in the public folder ## API -The core of `serve` is [serve-handler](https://github.com/vercel/serve-handler), which can be used as middleware in existing HTTP servers: +The core of `serve` is [`serve-handler`](https://github.com/vercel/serve-handler), which can be used as middleware in existing HTTP servers: ```js const handler = require('serve-handler'); @@ -77,9 +79,7 @@ server.listen(3000, () => { }); ``` -> **Note** -> -> You can also replace `http.createServer` with [micro](https://github.com/vercel/micro). +> You can also replace `http.createServer` with [`micro`](https://github.com/vercel/micro). ## Issues and Contributing @@ -89,7 +89,7 @@ If you wish to contribute to the project, please read the [contributing guide](c ## Credits -This project used to be called "list" and "micro-list". But thanks to [TJ Holowaychuk](https://github.com/tj) handing us the new name, it's now called "serve" (which is much more definite). +This project used to be called `list` and `micro-list`. But thanks to [TJ Holowaychuk](https://github.com/tj) handing us the new name, it's now called `serve` (which is much more definite). ## Author