From cce5b25053075edea3153b78f434e794b57c7f04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 07:43:12 +0000 Subject: [PATCH 1/6] chore(deps): bump yargs-parser from 20.2.9 to 21.0.1 (#3) Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 20.2.9 to 21.0.1. - [Release notes](https://github.com/yargs/yargs-parser/releases) - [Changelog](https://github.com/yargs/yargs-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs-parser/compare/yargs-parser-v20.2.9...yargs-parser-v21.0.1) --- updated-dependencies: - dependency-name: yargs-parser dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9bbfe21a..13eea659 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "resolve-from": "^5.0.0", "semver": "^7.3.5", "split2": "^4.1.0", - "yargs-parser": "^20.0.0" + "yargs-parser": "^21.0.1" }, "devDependencies": { "@fastify/autoload": "^5.0.0", From c0904b5ea84c11a66d611db42626c46644e071a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 07:50:18 +0000 Subject: [PATCH 2/6] chore(deps-dev): bump del-cli from 3.0.1 to 4.0.1 (#4) Bumps [del-cli](https://github.com/sindresorhus/del-cli) from 3.0.1 to 4.0.1. - [Release notes](https://github.com/sindresorhus/del-cli/releases) - [Commits](https://github.com/sindresorhus/del-cli/compare/v3.0.1...v4.0.1) --- updated-dependencies: - dependency-name: del-cli dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13eea659..ba928a39 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@types/node": "^17.0.8", "@types/tap": "^15.0.5", "concurrently": "^7.0.0", - "del-cli": "^3.0.1", + "del-cli": "^4.0.1", "fastify-plugin": "^3.0.0", "fastify-tsconfig": "^1.0.1", "minimatch": "^5.1.0", From ac4d0583f88ae8876c2209183a7c4543eed257fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 07:35:28 +0000 Subject: [PATCH 3/6] chore(deps): bump help-me from 3.0.0 to 4.0.0 (#8) Bumps [help-me](https://github.com/mcollina/help-me) from 3.0.0 to 4.0.0. - [Release notes](https://github.com/mcollina/help-me/releases) - [Commits](https://github.com/mcollina/help-me/commits/v4.0.0) --- updated-dependencies: - dependency-name: help-me dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba928a39..4a9001d7 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "dotenv": "^16.0.0", "fastify": "^4.0.0-rc.2", "generify": "^4.0.0", - "help-me": "^3.0.0", + "help-me": "^4.0.0", "is-docker": "^2.0.0", "make-promises-safe": "^5.1.0", "pino-colada": "^2.2.2", From b69599b697492d8f59a78b4043a68fc91a53bb5f Mon Sep 17 00:00:00 2001 From: KaKa Date: Fri, 27 May 2022 16:09:38 +0800 Subject: [PATCH 4/6] feat: remake preview --- .github/dependabot.yml | 13 - .github/stale.yml | 21 - .github/workflows/ci.yml | 15 - .gitignore | 107 +- .taprc | 10 +- README.md | 417 ++------ args.js | 79 -- bin/dev | 17 + bin/dev.cmd | 3 + bin/run | 5 + bin/run.cmd | 3 + cli.js | 36 - eject-ts.js | 42 - eject.js | 36 - examples/.env | 1 - examples/async-await-plugin.js | 7 - .../CJS-plugin-with-custom-options.cjs | 6 - .../ESM-plugin-with-custom-options.js | 10 - examples/package-type-module/package.json | 12 - examples/plugin-with-custom-options.js | 6 - examples/plugin-with-env.js | 7 - examples/plugin-with-options.js | 15 - examples/plugin-with-preloaded.js | 12 - examples/plugin.js | 12 - examples/ts-plugin-with-custom-options.js | 6 - examples/ts-plugin-with-custom-options.mjs | 8 - examples/ts-plugin-with-options.js | 15 - examples/ts-plugin.js | 12 - generate-plugin.js | 118 --- generate-readme.js | 138 --- generate.js | 171 --- help/eject-ts.txt | 5 - help/eject.txt | 5 - help/generate-plugin.txt | 5 - help/generate.txt | 13 - help/help.txt | 13 - help/print-routes.txt | 1 - help/readme.txt | 5 - help/start.txt | 63 -- helper.js | 15 - lib/watch/constants.js | 7 - lib/watch/fork.js | 56 - lib/watch/index.js | 117 --- lib/watch/utils.js | 26 - log.js | 21 - package.json | 136 ++- print-routes.js | 82 -- src/commands/generate/project.ts | 246 +++++ src/commands/start.ts | 53 + src/index.ts | 1 + src/utils/command/command.ts | 14 + src/utils/command/parser.ts | 149 +++ src/utils/import.ts | 12 + src/utils/package-json.ts | 192 ++++ src/utils/require.ts | 111 ++ src/utils/start.ts | 198 ++++ src/utils/watch/constants.ts | 1 + src/utils/watch/events.ts | 2 + src/utils/watch/fastify.ts | 55 + src/utils/watch/options.ts | 8 + src/utils/watch/watch.ts | 113 ++ src/utils/watch/watcher.ts | 5 + start.js | 179 ---- templates/app-ts/.vscode/launch.json | 20 - templates/app-ts/.vscode/tasks.json | 21 - templates/app-ts/README.md | 23 - templates/app-ts/__gitignore | 65 -- templates/app-ts/__taprc | 4 - templates/app-ts/test/tsconfig.json | 8 - templates/app-ts/tsconfig.json | 8 - templates/app/.vscode/launch.json | 19 - templates/app/__gitignore | 58 -- templates/eject-ts/server.ts | 38 - templates/eject/server.js | 40 - templates/plugin/.github/workflows/ci.yml | 19 - templates/plugin/.taprc | 3 - templates/plugin/README.md | 29 - templates/plugin/index.d.ts | 13 - templates/plugin/index.js | 9 - templates/plugin/test/index.test-d.ts | 13 - templates/plugin/test/index.test.js | 13 - templates/plugin/tsconfig.json | 9 - templates/{app => project}/README.md | 2 +- .../.gitignore => project/__gitignore.ejs} | 62 +- templates/{app => project}/app.js | 0 .../{app-ts/src => project}/plugins/README.md | 0 .../{app => project}/plugins/sensible.js | 0 templates/{app => project}/plugins/support.js | 0 templates/{app => project}/routes/README.md | 2 +- .../{app => project}/routes/example/index.js | 0 templates/{app => project}/routes/root.js | 0 templates/{app-ts => project}/src/app.ts | 17 +- .../{app => project/src}/plugins/README.md | 0 .../src/plugins/sensible.ts | 6 +- .../src/plugins/support.ts | 5 +- .../{app-ts => project}/src/routes/README.md | 7 +- .../src/routes/example/index.ts | 4 +- .../{app-ts => project}/src/routes/root.ts | 2 +- templates/{app => project}/test/helper.js | 0 templates/{app-ts => project}/test/helper.ts | 17 +- .../test/plugins/support.test.js | 0 .../test/plugins/support.test.ts | 2 +- .../test/routes/example.test.js | 0 .../test/routes/example.test.ts | 0 .../{app => project}/test/routes/root.test.js | 0 .../test/routes/root.test.ts | 0 templates/project/tsconfig.build.json | 22 + templates/project/tsconfig.json | 20 + templates/readme/README.md | 52 - test/args.test.js | 255 ----- test/cli.test.js | 19 - test/commands/generate/project.test.ts | 174 ++++ test/data/async-plugin-with-one-argument.js | 6 - test/data/custom-logger.js | 6 - test/data/custom-require.js | 3 - test/data/custom-require2.js | 3 - test/data/package-not-found.js | 3 - test/data/parsing-error.js | 1 - test/data/rejection.js | 7 - test/data/timeout-plugin.js | 10 - test/data/undefinedVariable.js | 1 - test/eject-ts.test.js | 92 -- test/eject.test.js | 90 -- test/env.ts | 5 + test/generate-plugin.test.js | 173 --- test/generate-readme.test.js | 26 - test/generate-typescript.test.js | 191 ---- test/generate.test.js | 186 ---- test/graceful-shutdown.test.js | 54 - test/helper.test.js | 101 -- test/plugindir/package.json | 14 - test/plugindir/plugin.js | 10 - test/print-routes.test.js | 113 -- test/run-command.ts | 116 +++ test/sleep.ts | 5 + test/start.test.js | 983 ------------------ test/templates/app-ts.test.ts | 55 - test/watch.test.js | 15 - tsconfig.eslint.json | 21 + tsconfig.json | 34 +- util.js | 101 -- 141 files changed, 1921 insertions(+), 4853 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/stale.yml delete mode 100644 .github/workflows/ci.yml delete mode 100644 args.js create mode 100644 bin/dev create mode 100644 bin/dev.cmd create mode 100644 bin/run create mode 100644 bin/run.cmd delete mode 100755 cli.js delete mode 100644 eject-ts.js delete mode 100644 eject.js delete mode 100644 examples/.env delete mode 100644 examples/async-await-plugin.js delete mode 100644 examples/package-type-module/CJS-plugin-with-custom-options.cjs delete mode 100644 examples/package-type-module/ESM-plugin-with-custom-options.js delete mode 100644 examples/package-type-module/package.json delete mode 100644 examples/plugin-with-custom-options.js delete mode 100644 examples/plugin-with-env.js delete mode 100644 examples/plugin-with-options.js delete mode 100644 examples/plugin-with-preloaded.js delete mode 100644 examples/plugin.js delete mode 100644 examples/ts-plugin-with-custom-options.js delete mode 100644 examples/ts-plugin-with-custom-options.mjs delete mode 100644 examples/ts-plugin-with-options.js delete mode 100644 examples/ts-plugin.js delete mode 100755 generate-plugin.js delete mode 100644 generate-readme.js delete mode 100755 generate.js delete mode 100644 help/eject-ts.txt delete mode 100644 help/eject.txt delete mode 100644 help/generate-plugin.txt delete mode 100644 help/generate.txt delete mode 100644 help/help.txt delete mode 100644 help/print-routes.txt delete mode 100644 help/readme.txt delete mode 100644 help/start.txt delete mode 100644 helper.js delete mode 100644 lib/watch/constants.js delete mode 100644 lib/watch/fork.js delete mode 100644 lib/watch/index.js delete mode 100644 lib/watch/utils.js delete mode 100644 log.js delete mode 100644 print-routes.js create mode 100644 src/commands/generate/project.ts create mode 100644 src/commands/start.ts create mode 100644 src/index.ts create mode 100644 src/utils/command/command.ts create mode 100644 src/utils/command/parser.ts create mode 100644 src/utils/import.ts create mode 100644 src/utils/package-json.ts create mode 100644 src/utils/require.ts create mode 100644 src/utils/start.ts create mode 100644 src/utils/watch/constants.ts create mode 100644 src/utils/watch/events.ts create mode 100644 src/utils/watch/fastify.ts create mode 100644 src/utils/watch/options.ts create mode 100644 src/utils/watch/watch.ts create mode 100644 src/utils/watch/watcher.ts delete mode 100755 start.js delete mode 100644 templates/app-ts/.vscode/launch.json delete mode 100644 templates/app-ts/.vscode/tasks.json delete mode 100644 templates/app-ts/README.md delete mode 100644 templates/app-ts/__gitignore delete mode 100644 templates/app-ts/__taprc delete mode 100644 templates/app-ts/test/tsconfig.json delete mode 100644 templates/app-ts/tsconfig.json delete mode 100644 templates/app/.vscode/launch.json delete mode 100644 templates/app/__gitignore delete mode 100644 templates/eject-ts/server.ts delete mode 100644 templates/eject/server.js delete mode 100644 templates/plugin/.github/workflows/ci.yml delete mode 100644 templates/plugin/.taprc delete mode 100644 templates/plugin/README.md delete mode 100644 templates/plugin/index.d.ts delete mode 100644 templates/plugin/index.js delete mode 100644 templates/plugin/test/index.test-d.ts delete mode 100644 templates/plugin/test/index.test.js delete mode 100644 templates/plugin/tsconfig.json rename templates/{app => project}/README.md (94%) rename templates/{plugin/.gitignore => project/__gitignore.ejs} (53%) rename templates/{app => project}/app.js (100%) rename templates/{app-ts/src => project}/plugins/README.md (100%) rename templates/{app => project}/plugins/sensible.js (100%) rename templates/{app => project}/plugins/support.js (100%) rename templates/{app => project}/routes/README.md (96%) rename templates/{app => project}/routes/example/index.js (100%) rename templates/{app => project}/routes/root.js (100%) rename templates/{app-ts => project}/src/app.ts (74%) rename templates/{app => project/src}/plugins/README.md (100%) rename templates/{app-ts => project}/src/plugins/sensible.ts (70%) rename templates/{app-ts => project}/src/plugins/support.ts (72%) rename templates/{app-ts => project}/src/routes/README.md (76%) rename templates/{app-ts => project}/src/routes/example/index.ts (71%) rename templates/{app-ts => project}/src/routes/root.ts (90%) rename templates/{app => project}/test/helper.js (100%) rename templates/{app-ts => project}/test/helper.ts (60%) rename templates/{app => project}/test/plugins/support.test.js (100%) rename templates/{app-ts => project}/test/plugins/support.test.ts (100%) rename templates/{app => project}/test/routes/example.test.js (100%) rename templates/{app-ts => project}/test/routes/example.test.ts (100%) rename templates/{app => project}/test/routes/root.test.js (100%) rename templates/{app-ts => project}/test/routes/root.test.ts (100%) create mode 100644 templates/project/tsconfig.build.json create mode 100644 templates/project/tsconfig.json delete mode 100644 templates/readme/README.md delete mode 100644 test/args.test.js delete mode 100644 test/cli.test.js create mode 100644 test/commands/generate/project.test.ts delete mode 100644 test/data/async-plugin-with-one-argument.js delete mode 100644 test/data/custom-logger.js delete mode 100644 test/data/custom-require.js delete mode 100644 test/data/custom-require2.js delete mode 100644 test/data/package-not-found.js delete mode 100644 test/data/parsing-error.js delete mode 100644 test/data/rejection.js delete mode 100644 test/data/timeout-plugin.js delete mode 100644 test/data/undefinedVariable.js delete mode 100644 test/eject-ts.test.js delete mode 100644 test/eject.test.js create mode 100644 test/env.ts delete mode 100644 test/generate-plugin.test.js delete mode 100644 test/generate-readme.test.js delete mode 100644 test/generate-typescript.test.js delete mode 100644 test/generate.test.js delete mode 100644 test/graceful-shutdown.test.js delete mode 100644 test/helper.test.js delete mode 100644 test/plugindir/package.json delete mode 100644 test/plugindir/plugin.js delete mode 100644 test/print-routes.test.js create mode 100644 test/run-command.ts create mode 100644 test/sleep.ts delete mode 100644 test/start.test.js delete mode 100644 test/templates/app-ts.test.ts delete mode 100644 test/watch.test.js create mode 100644 tsconfig.eslint.json delete mode 100644 util.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index dfa7fa6c..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - open-pull-requests-limit: 10 - - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index d51ce639..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 15 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - "discussion" - - "feature request" - - "bug" - - "help wanted" - - "plugin suggestion" - - "good first issue" -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 32b3d169..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: CI - -on: - push: - paths-ignore: - - 'docs/**' - - '*.md' - pull_request: - paths-ignore: - - 'docs/**' - - '*.md' - -jobs: - test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 diff --git a/.gitignore b/.gitignore index 0df09546..927ef02f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,56 +2,129 @@ logs *.log npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed +*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage +*.lcov # nyc test coverage .nyc_output -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt +# Bower dependency directory (https://bower.io/) +bower_components + # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) +# Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories -node_modules -jspm_packages +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo # Optional npm cache directory .npm +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + # Optional REPL history .node_repl_history -# mac files -.DS_Store +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist -# vim swap files +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files *.swp -# remove lock files +# macOS files +.DS_Store + +# lock files package-lock.json yarn.lock -# created by test -test.sock - -# test artifacts -test/workdir -test/fixtures/*.js -.vscode/launch.json -.vscode/settings.json -templates/app-ts/dist +# editor files +.vscode +.idea diff --git a/.taprc b/.taprc index 85101415..eeecae96 100644 --- a/.taprc +++ b/.taprc @@ -1,6 +1,6 @@ ts: true -jsx: false -jobs: 1 -reporter: terse -check-coverage: false -timeout: 60 +no-timeout: true +statements: 10 +branches: 10 +functions: 10 +lines: 10 \ No newline at end of file diff --git a/README.md b/README.md index 189fb640..a97890a8 100644 --- a/README.md +++ b/README.md @@ -1,360 +1,103 @@ -# fastify-cli -![CI](https://github.com/fastify/fastify-cli/workflows/CI/badge.svg) -[![NPM version](https://img.shields.io/npm/v/fastify-cli.svg?style=flat)](https://www.npmjs.com/package/fastify-cli) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) -Command line tools for [Fastify](https://github.com/fastify/fastify). -Generate, write, and run an application with one single command! - -## Install -```bash -npm install fastify-cli --global + +* [Usage](#usage) +* [Commands](#commands) + +# Usage + +```sh-session +$ npm install -g fastify-cli +$ fastify COMMAND +running command... +$ fastify (--version) +fastify-cli/3.1.0 linux-x64 node-v18.2.0 +$ fastify --help [COMMAND] +USAGE + $ fastify COMMAND +... ``` + +# Commands + +* [`fastify generate project [NAME]`](#fastify-generate-project-name) +* [`fastify help [COMMAND]`](#fastify-help-command) +* [`fastify start ENTRY`](#fastify-start-entry) -## Usage +## `fastify generate project [NAME]` -`fastify-cli` offers a single command line interface for your Fastify -project: +Generate fastify project -```bash -$ fastify ``` - -Will print an help: - +USAGE + $ fastify generate project [NAME] [--location ] [--overwrite] [--language ] [--lint ] [--test + ] [--help] + +ARGUMENTS + NAME Name of the project. + +FLAGS + --help Show CLI help. + --language= Programming Language you would like to use in this project. + --lint= Lint Tools you would like to use in this project. + --location= Location to place the project. + --overwrite Force to overwrite the project location when it exist. + --test= Test Framework you would like to use in this project. + +DESCRIPTION + Generate fastify project ``` -Fastify command line interface, available commands are: - * start start a server - * generate generate a new project - * generate-plugin generate a new plugin project - * readme generate a README.md for the plugin - * print-routes prints the representation of the internal radix tree used by the router, useful for debugging. - * version the current fastify-cli version - * help help about commands +## `fastify help [COMMAND]` -Launch 'fastify help [command]' to know more about the commands. +Display help for fastify. -The default command is start, you can hit - - fastify start plugin.js - -to start plugin.js. ``` +USAGE + $ fastify help [COMMAND] [-n] -### start - -You can start any Fastify plugin with: - -```bash -$ fastify start plugin.js -``` - -A plugin can be as simple as: - -```js -// plugin.js -module.exports = function (fastify, options, next) { - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} -``` - -If you are using Node 8+, you can use `Promises` or `async` functions too: - -```js -// async-await-plugin.js -module.exports = async function (fastify, options) { - fastify.get('/', async function (req, reply) { - return { hello: 'world' } - }) -} -``` - -For a list of available flags for `fastify start` see the help: `fastify help start`. - -If you want to use custom options for the server creation, just export an options object with your route and run the cli command with the `--options` flag. - -```js -// plugin.js -module.exports = function (fastify, options, next) { - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} - -module.exports.options = { - https: { - key: 'key', - cert: 'cert' - } -} -``` - -If you want to use custom options for your plugin, just add them after the `--` terminator. - -```js -// plugin.js -module.exports = function (fastify, options, next) { - if (option.one) { - //... - } - //... - next() -} -``` - -```bash -$ fastify start plugin.js -- --one -``` - -Modules in EcmaScript Module format can be used on Node.js >= 14 or >= 12.17.0 but < 13.0.0' -```js -// plugin.js -export default async function plugin (fastify, options) { - fastify.get('/', async function (req, reply) { - return options - }) -} -``` - -This works with a `.js` extension if you are using Node.js >= 14 and the nearest parent `package.json` has `"type": "module"` -([more info here](https://nodejs.medium.com/announcing-core-node-js-support-for-ecmascript-modules-c5d6dc29b663)). -If your `package.json` does not have `"type": "module"`, use `.mjs` for the extension (`plugin.mjs` in the above example). - -#### Options -You can pass the following options via CLI arguments. Every option has a corresponding environment variable: - -| Description | Short command | Full command | Environment variable | -| --------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------ | ------------------------ | -| Port to listen on (default to 3000) | `-p` | `--port` | `FASTIFY_PORT or PORT` | -| Address to listen on | `-a` | `--address` | `FASTIFY_ADDRESS` | -| Socket to listen on | `-s` | `--socket` | `FASTIFY_SOCKET` | -| Module to preload | `-r` | `--require` | `FASTIFY_REQUIRE` | -| Log level (default to fatal) | `-l` | `--log-level` | `FASTIFY_LOG_LEVEL` | -| Path to logging configuration module to use | `-L` | `--logging-module` | `FASTIFY_LOGGING_MODULE` | -| Start Fastify app in debug mode with nodejs inspector | `-d` | `--debug` | `FASTIFY_DEBUG` | -| Set the inspector port (default: 9320) | `-I` | `--debug-port` | `FASTIFY_DEBUG_PORT` | -| Set the inspector host to listen on (default: loopback address or `0.0.0.0` inside Docker) | | `--debug-host` | `FASTIFY_DEBUG_HOST` | -| Prints pretty logs | `-P` | `--pretty-logs` | `FASTIFY_PRETTY_LOGS` | -| Watch process.cwd() directory for changes, recursively; when that happens, the process will auto reload | `-w` | `--watch` | `FASTIFY_WATCH` | -| Ignore changes to the specified files or directories when watch is enabled. (e.g. `--ignore-watch='node_modules .git logs/error.log'` ) | | `--ignore-watch` | `FASTIFY_IGNORE_WATCH` | -| Prints events triggered by watch listener (useful to debug unexpected reload when using `--watch` ) | | `--verbose-watch` | `FASTIFY_VERBOSE_WATCH` | -| Use custom options | `-o` | `--options` | `FASTIFY_OPTIONS` | -| Set the prefix | `-x` | `--prefix` | `FASTIFY_PREFIX` | -| Set the plugin timeout | `-T` | `--plugin-timeout` | `FASTIFY_PLUGIN_TIMEOUT` | -| Defines the maximum payload, in bytes,
that the server is allowed to accept | | `--body-limit` | `FASTIFY_BODY_LIMIT` | - -By default `fastify-cli` runs [`dotenv`](https://www.npmjs.com/package/dotenv), so it will load all the env variables stored in `.env` in your current working directory. - -The default value for `--plugin-timeout` is 10 seconds. -By default `--ignore-watch` flag is set to ignore `node_modules build dist .git bower_components logs .swp' files. - -#### Containerization - -When deploying to a Docker, and potentially other, containers, it is advisable to set a fastify address of `0.0.0.0` because these containers do not default to exposing mapped ports to localhost. - -For containers built and run specifically by the Docker Daemon, fastify-cli is able to detect that the server process is running within a Docker container and the `0.0.0.0` listen address is set automatically. - -Other containerization tools (eg. Buildah and Podman) are not detected automatically, so the `0.0.0.0` listen address must be set explicitly with either the `--address` flag or the `FASTIFY_ADDRESS` environment variable. - -#### Fastify version discovery - -If Fastify is installed as a project dependency (with `npm install --save fastify`), -then `fastify-cli` will use that version of Fastify when running the server. -Otherwise, `fastify-cli` will use the version of Fastify included within `fastify-cli`. - -#### Migrating out of fastify-cli start - -If you would like to turn your application into a standalone executable, -just add the following `server.js`: - -```js -'use strict' - -// Read the .env file. -require('dotenv').config() +ARGUMENTS + COMMAND Command to show help for. -// Require the framework -const Fastify = require('fastify') +FLAGS + -n, --nested-commands Include all nested commands in the output. -// Require library to exit fastify process, gracefully (if possible) -const closeWithGrace = require('close-with-grace') - -// Instantiate Fastify with some config -const app = Fastify({ - logger: true -}) - -// Register your application as a normal plugin. -const appService = require('./app.js') -app.register(appService) - -// delay is the number of milliseconds for the graceful close to finish -const closeListeners = closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { - if (err) { - app.log.error(err) - } - await app.close() -}) - -app.addHook('onClose', (instance, done) => { - closeListeners.uninstall() - done() -}) - -// Start listening. -app.listen({ port: process.env.PORT || 3000 }, (err) => { - if (err) { - app.log.error(err) - process.exit(1) - } -}) +DESCRIPTION + Display help for fastify. ``` -#### Unhandled rejections - -fastify-cli uses [make-promises-safe](https://github.com/mcollina/make-promises-safe) to avoid memory leaks -in case of an `'unhandledRejection'`. - -### generate - -`fastify-cli` can also help with generating some project scaffolding to -kickstart the development of your next Fastify application. To use it: - -1. `fastify generate ` -2. `cd yourapp` -3. `npm install` - -The sample code offers you the following npm tasks: - -* `npm start` - starts the application -* `npm run dev` - starts the application with - [`pino-colada`](https://github.com/lrlna/pino-colada) pretty logging - (not suitable for production) -* `npm test` - runs the tests - -You will find three different folders: -- `plugins`: the folder where you will place all your custom plugins -- `routes`: the folder where you will declare all your endpoints -- `test`: the folder where you will declare all your test - -Finally, there will be an `app.js` file, which is your entry point. -It is a standard Fastify plugin and you will not need to add the `listen` method to run the server, just run it with one of the scripts above. - -If the target directory exists `fastify generate` will fail unless the target directory is `.`, as in the current directory. - -If the target directory is the current directory (`.`) and it already contains a `package.json` file, `fastify generate` will fail. This can -be overidden with the `--integrate` flag: - -`fastify generate . --integrate` - -This will add or alter the `main`, `scripts`, `dependencies` and `devDependencies` fields on the `package.json`. In cases of file name collisions -for any files being added, the file will be overwritten with the new file added by `fastify generate`. If there is an existing `app.js` in this scenario, -it will be overwritten. Use the `--integrate` flag with care. - -#### Options - -| Description | Full command | -| --- | --- | -| Use the TypeScript template | `--lang=ts`, `--lang=typescript` | -| Overwrite it when the target directory is the current directory (`.`) | `--integrate`| - -### generate-plugin - -`fastify-cli` can help you improve your plugin development by generating a scaffolding project: +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_ -1. `fastify generate-plugin ` -2. `cd yourplugin` -3. `npm install` +## `fastify start ENTRY` -The boilerplate provides some useful npm scripts: -* `npm run unit`: runs all unit tests -* `npm run lint`: to check your project's code style -* `npm run test:typescript`: runs types tests -* `npm test`: runs all the checks at once +Start fastify instance -### readme - -`fastify-cli` can also help with generating a concise and informative readme for your plugin. If no `package.json` is provided a new one is generated automatically. -To use it: - -1. `cd yourplugin` -2. `fastify readme ` - -Finally, there will be a new `README.md` file, which provides internal information about your plugin e.g: - -* Install instructions -* Example usage -* Plugin dependencies -* Exposed decorators -* Encapsulation semantics -* Compatible Fastify version - -### linting - -`fastify-cli` is unopinionated on the choice of linter. We recommend you to add a linter, like so: - -```diff -"devDependencies": { -+ "standard": "^11.0.1", -} - -"scripts": { -+ "pretest": "standard", - "test": "tap test/**/*.test.js", - "start": "fastify start -l info app.js", - "dev": "fastify start -l info -P app.js", -+ "lint": "standard --fix" -}, ``` - -## Test helpers - -When you use `fastify-cli` to run your project you need a way to load your application because you can run the CLI command. -To do so, you can use the this module to load your application and give you the control to write your assertions. -These utilities are async functions that you may use with the [`node-tap`](https://www.npmjs.com/package/tap) testing framework. - -There are two utilities provided: - -- `build`: builds your application and returns the `fastify` instance without calling the `listen` method. -- `listen`: starts your application and returns the `fastify` instance listening on the configured port. - -Both of these utilities have the `function(arg, pluginOptions)` parameters: - -- `cliArgs`: is a string or a string array within the same arguments passed to the `fastify-cli` command. -- `pluginOptions`: is an object containing the options provided to the started plugin (eg: `app.js`). - -```js -// load the utility helper functions -const { build, listen } = require('fastify-cli/helper') - -// write a test -const { test } = require('tap') -test('test my application', async t => { - const argv = ['app.js'] - const app = await build(argv, { - extraParam: 'foo' - }) - t.teardown(() => app.close()) - - // test your application here: - const res = await app.inject('/') - t.same(res.json(), { hello: 'one' }) -}) +USAGE + $ fastify start [ENTRY] [-r ] [-p ] [-a ] [--debug-port -d] [--debug-address + ] [--prefix ] [-P] [--watch] [--watch-ignore ] [--watch-verbose] [--help] + +ARGUMENTS + ENTRY Entry point of fastify instance. + +FLAGS + -P, --pretty-logs [default: false] Use "pino-pretty" for log display. It require to install the module + seperately. + -a, --address= [default: localhost] Address listen on. It can be either address or socket. + -d, --debug [default: false] Enable debug mode. + -p, --port= [default: 3000] Port listen on. + -r, --require=... Preload Modules, for example "-r ts-node/register". + --debug-address= Inspector host, by default it will be either "localhost" or "0.0.0.0" in docker. + --debug-port= [default: 9320] Inspector port. + --help Show CLI help. + --prefix= [default: ""] Entry file prefix. + --watch + --watch-ignore= + --watch-verbose + +DESCRIPTION + Start fastify instance ``` - -## Contributing -If you feel you can help in any way, be it with examples, extra testing, or new features please open a pull request or open an issue. - -### How to execute the CLI -Instead of using the `fastify` keyword before each command, use `node cli.js` -
Example: replace `fastify start` with `node cli.js start` - -## License -**[MIT](https://github.com/fastify/fastify-cli/blob/master/LICENSE)** +_See code: [dist/commands/start.ts](https://github.com/fastify/fastify-cli/blob/v3.1.0/dist/commands/start.ts)_ + diff --git a/args.js b/args.js deleted file mode 100644 index 8a7edb98..00000000 --- a/args.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict' - -const argv = require('yargs-parser') -const dotenv = require('dotenv') - -const DEFAULT_IGNORE = 'node_modules build dist .git bower_components logs .swp .nyc_output' - -module.exports = function parseArgs (args) { - dotenv.config() - const parsedArgs = argv(args, { - configuration: { - 'populate--': true - }, - number: ['port', 'inspect-port', 'body-limit', 'plugin-timeout'], - string: ['log-level', 'address', 'socket', 'prefix', 'ignore-watch', 'logging-module', 'debug-host', 'lang', 'require'], - boolean: ['pretty-logs', 'options', 'watch', 'verbose-watch', 'debug'], - envPrefix: 'FASTIFY_', - alias: { - port: ['p'], - socket: ['s'], - help: ['h'], - options: ['o'], - address: ['a'], - watch: ['w'], - prefix: ['x'], - require: ['r'], - debug: ['d'], - 'debug-port': ['I'], - 'log-level': ['l'], - 'pretty-logs': ['P'], - 'plugin-timeout': ['T'], - 'logging-module': ['L'] - }, - default: { - 'log-level': 'fatal', - 'pretty-logs': false, - watch: false, - verboseWatch: false, - debug: false, - debugPort: 9320, - options: false, - 'plugin-timeout': 10 * 1000, // everything should load in 10 seconds - lang: 'js' - } - }) - - const additionalArgs = parsedArgs['--'] || [] - const { _, ...pluginOptions } = argv(additionalArgs) - const ignoreWatchArg = parsedArgs.ignoreWatch || '' - - let ignoreWatch = `${DEFAULT_IGNORE} ${ignoreWatchArg}`.trim() - if (ignoreWatchArg.includes('.ts$')) { - ignoreWatch = ignoreWatch.replace('dist', '') - } - - return { - _: parsedArgs._, - '--': additionalArgs, - port: parsedArgs.port, - bodyLimit: parsedArgs.bodyLimit, - pluginTimeout: parsedArgs.pluginTimeout, - pluginOptions, - prettyLogs: parsedArgs.prettyLogs, - options: parsedArgs.options, - watch: parsedArgs.watch, - debug: parsedArgs.debug, - debugPort: parsedArgs.debugPort, - debugHost: parsedArgs.debugHost, - ignoreWatch, - verboseWatch: parsedArgs.verboseWatch, - logLevel: parsedArgs.logLevel, - address: parsedArgs.address, - socket: parsedArgs.socket, - require: parsedArgs.require, - prefix: parsedArgs.prefix, - loggingModule: parsedArgs.loggingModule, - lang: parsedArgs.lang - } -} diff --git a/bin/dev b/bin/dev new file mode 100644 index 00000000..d5b53266 --- /dev/null +++ b/bin/dev @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +const path = require('path') +const project = path.join(__dirname, '..', 'tsconfig.json') + +// In dev mode -> use ts-node and dev plugins +process.env.NODE_ENV = 'development' + +require('ts-node').register({ project }) + +// In dev mode, always show stack traces +oclif.settings.debug = true + +// Start the CLI +oclif.run().then(oclif.flush).catch(oclif.Errors.handle) diff --git a/bin/dev.cmd b/bin/dev.cmd new file mode 100644 index 00000000..077b57ae --- /dev/null +++ b/bin/dev.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\dev" %* \ No newline at end of file diff --git a/bin/run b/bin/run new file mode 100644 index 00000000..a7635de8 --- /dev/null +++ b/bin/run @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) diff --git a/bin/run.cmd b/bin/run.cmd new file mode 100644 index 00000000..968fc307 --- /dev/null +++ b/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* diff --git a/cli.js b/cli.js deleted file mode 100755 index c351182b..00000000 --- a/cli.js +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -const path = require('path') -const commist = require('commist')() -const help = require('help-me')({ - // the default - dir: path.join(path.dirname(require.main.filename), 'help') -}) -const start = require('./start') -const eject = require('./eject') -const ejectTs = require('./eject-ts') -const generate = require('./generate') -const generatePlugin = require('./generate-plugin') -const generateReadme = require('./generate-readme') -const printRoutes = require('./print-routes') - -commist.register('start', start.cli) -commist.register('eject', eject.cli) -commist.register('eject-ts', ejectTs.cli) -commist.register('generate', generate.cli) -commist.register('generate-plugin', generatePlugin.cli) -commist.register('readme', generateReadme.cli) -commist.register('help', help.toStdout) -commist.register('version', function () { - console.log(require('./package.json').version) -}) -commist.register('print-routes', printRoutes.cli) - -const res = commist.parse(process.argv.splice(2)) - -if (res) { - // no command was recognized - help.toStdout(res) -} diff --git a/eject-ts.js b/eject-ts.js deleted file mode 100644 index 4147df1a..00000000 --- a/eject-ts.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const path = require('path') -const generify = require('generify') -const log = require('./log') - -function eject (dir) { - return new Promise((resolve, reject) => { - generify( - path.join(__dirname, 'templates', 'eject-ts'), - dir, - {}, - function (file) { - log('debug', `generated ${file}`) - }, - function (err) { - if (err) { - return reject(err) - } - resolve() - } - ) - }) -} - -function cli () { - eject(process.cwd()).catch(function (err) { - if (err) { - log('error', err.message) - process.exit(1) - } - }) -} - -module.exports = { - eject, - cli -} - -if (require.main === module) { - cli() -} diff --git a/eject.js b/eject.js deleted file mode 100644 index d76c5445..00000000 --- a/eject.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -const path = require('path') -const generify = require('generify') -const log = require('./log') - -function eject (dir) { - return new Promise((resolve, reject) => { - generify(path.join(__dirname, 'templates', 'eject'), dir, {}, function (file) { - log('debug', `generated ${file}`) - }, function (err) { - if (err) { - return reject(err) - } - resolve() - }) - }) -} - -function cli () { - eject(process.cwd()).catch(function (err) { - if (err) { - log('error', err.message) - process.exit(1) - } - }) -} - -module.exports = { - eject, - cli -} - -if (require.main === module) { - cli() -} diff --git a/examples/.env b/examples/.env deleted file mode 100644 index b39061ac..00000000 --- a/examples/.env +++ /dev/null @@ -1 +0,0 @@ -GREETING=world diff --git a/examples/async-await-plugin.js b/examples/async-await-plugin.js deleted file mode 100644 index b0629de7..00000000 --- a/examples/async-await-plugin.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = async function (fastify, options) { - fastify.get('/', async function (req, reply) { - return { hello: 'world' } - }) -} diff --git a/examples/package-type-module/CJS-plugin-with-custom-options.cjs b/examples/package-type-module/CJS-plugin-with-custom-options.cjs deleted file mode 100644 index 5d8fc7a6..00000000 --- a/examples/package-type-module/CJS-plugin-with-custom-options.cjs +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -exports.default = function (fastify, options, next) { - fastify.get('/', (req, reply) => reply.send(options)) - next() -} diff --git a/examples/package-type-module/ESM-plugin-with-custom-options.js b/examples/package-type-module/ESM-plugin-with-custom-options.js deleted file mode 100644 index d8c0832c..00000000 --- a/examples/package-type-module/ESM-plugin-with-custom-options.js +++ /dev/null @@ -1,10 +0,0 @@ -// Module code is always strict mode code. -// http://www.ecma-international.org/ecma-262/6.0/#sec-strict-mode-code -// -// this file has a .js extension, but the package.json in this folder contains '"type":"module"' - -export default async function plugin (fastify, options) { - fastify.get('/', async function (req, reply) { - return options - }) -} diff --git a/examples/package-type-module/package.json b/examples/package-type-module/package.json deleted file mode 100644 index 2e64cf34..00000000 --- a/examples/package-type-module/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "esm-plugin-as-js", - "version": "1.0.0", - "description": "example of a ESM plugin with js filetype and type module", - "main": "ESM-plugin-with-custom-options.js", - "type":"module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "MIT" -} diff --git a/examples/plugin-with-custom-options.js b/examples/plugin-with-custom-options.js deleted file mode 100644 index 0be62b23..00000000 --- a/examples/plugin-with-custom-options.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -module.exports = function (fastify, options, next) { - fastify.get('/', (req, reply) => reply.send(options)) - next() -} diff --git a/examples/plugin-with-env.js b/examples/plugin-with-env.js deleted file mode 100644 index 1c2a9c65..00000000 --- a/examples/plugin-with-env.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = async function (fastify, options) { - fastify.get('/', async function (req, reply) { - return { hello: process.env.GREETING } - }) -} diff --git a/examples/plugin-with-options.js b/examples/plugin-with-options.js deleted file mode 100644 index 75edbb1b..00000000 --- a/examples/plugin-with-options.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -module.exports = function (fastify, options, next) { - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} - -module.exports.options = { - https: { - key: 'key', - cert: 'cert' - } -} diff --git a/examples/plugin-with-preloaded.js b/examples/plugin-with-preloaded.js deleted file mode 100644 index 9b8435dd..00000000 --- a/examples/plugin-with-preloaded.js +++ /dev/null @@ -1,12 +0,0 @@ -/* global GLOBAL_MODULE_1 */ -'use strict' -const t = require('tap') - -module.exports = async function (fastify, options) { - fastify.get('/', async function (req, reply) { - return { hasPreloaded: GLOBAL_MODULE_1 } - }) - fastify.addHook('onReady', function () { - t.ok(GLOBAL_MODULE_1) - }) -} diff --git a/examples/plugin.js b/examples/plugin.js deleted file mode 100644 index 949a803d..00000000 --- a/examples/plugin.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -module.exports = function (fastify, options, next) { - fastify.decorate('test', true) - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - fastify.post('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} diff --git a/examples/ts-plugin-with-custom-options.js b/examples/ts-plugin-with-custom-options.js deleted file mode 100644 index 5d8fc7a6..00000000 --- a/examples/ts-plugin-with-custom-options.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -exports.default = function (fastify, options, next) { - fastify.get('/', (req, reply) => reply.send(options)) - next() -} diff --git a/examples/ts-plugin-with-custom-options.mjs b/examples/ts-plugin-with-custom-options.mjs deleted file mode 100644 index 39b60b54..00000000 --- a/examples/ts-plugin-with-custom-options.mjs +++ /dev/null @@ -1,8 +0,0 @@ -// Module code is always strict mode code. -// http://www.ecma-international.org/ecma-262/6.0/#sec-strict-mode-code - -export default async function plugin (fastify, options) { - fastify.get('/', async function (req, reply) { - return options - }) -} diff --git a/examples/ts-plugin-with-options.js b/examples/ts-plugin-with-options.js deleted file mode 100644 index be33ef30..00000000 --- a/examples/ts-plugin-with-options.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -exports.default = function (fastify, options, next) { - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} - -exports.options = { - https: { - key: 'key', - cert: 'cert' - } -} diff --git a/examples/ts-plugin.js b/examples/ts-plugin.js deleted file mode 100644 index 5463c6ad..00000000 --- a/examples/ts-plugin.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -exports.default = function (fastify, options, next) { - fastify.decorate('test', true) - fastify.get('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - fastify.post('/', function (req, reply) { - reply.send({ hello: 'world' }) - }) - next() -} diff --git a/generate-plugin.js b/generate-plugin.js deleted file mode 100755 index ff67bf73..00000000 --- a/generate-plugin.js +++ /dev/null @@ -1,118 +0,0 @@ -'use strict' - -const { - readFile, - writeFile -} = require('fs').promises -const { existsSync } = require('fs') -const path = require('path') -const chalk = require('chalk') -const generify = require('generify') -const argv = require('yargs-parser') -const cliPkg = require('./package') -const { execSync } = require('child_process') -const { promisify } = require('util') -const log = require('./log') - -const pluginTemplate = { - dir: 'plugin', - main: 'index.js', - types: 'index.d.ts', - scripts: { - lint: 'standard && npm run lint:typescript', - 'lint:typescript': 'ts-standard', - test: 'npm run lint && npm run unit && npm run test:typescript', - 'test:typescript': 'tsd', - unit: 'tap test/**/*.test.js' - }, - dependencies: { - 'fastify-plugin': cliPkg.devDependencies['fastify-plugin'] - }, - devDependencies: { - '@types/node': cliPkg.devDependencies['@types/node'], - fastify: cliPkg.devDependencies.fastify, - 'fastify-tsconfig': cliPkg.devDependencies['fastify-tsconfig'], - standard: cliPkg.devDependencies.standard, - tap: cliPkg.devDependencies.tap, - 'ts-standard': cliPkg.devDependencies['ts-standard'], - tsd: cliPkg.devDependencies.tsd, - typescript: cliPkg.devDependencies.typescript - }, - tsd: { - directory: 'test' - }, - logInstructions: function (pkg) { - log('debug', 'saved package.json') - log('info', `project ${pkg.name} generated successfully`) - log('debug', `run '${chalk.bold('npm install')}' to install the dependencies`) - log('debug', `run '${chalk.bold('npm test')}' to execute the tests`) - } -} - -async function generate (dir, template) { - const generifyPromise = promisify(generify) - const file = await generifyPromise( - path.join(__dirname, 'templates', template.dir), - dir, - {} - ) - log('debug', `generated ${file}`) - - process.chdir(dir) - execSync('npm init -y') - - log('info', `reading package.json in ${dir}`) - - const pkg = await readFile('package.json').then(JSON.parse) - pkg.main = template.main - pkg.types = template.types - pkg.description = '' - pkg.license = 'MIT' - pkg.scripts = Object.assign(pkg.scripts || {}, template.scripts) - pkg.dependencies = Object.assign(pkg.dependencies || {}, template.dependencies) - pkg.devDependencies = Object.assign(pkg.devDependencies || {}, template.devDependencies) - pkg.tsd = Object.assign(pkg.tsd || {}, template.tsd) - - log('debug', 'edited package.json, saving') - - await writeFile('package.json', JSON.stringify(pkg, null, 2)) - - template.logInstructions(pkg) -} - -function cli (args) { - const opts = argv(args) - const dir = opts._[0] - - if (dir && existsSync(dir)) { - if (dir !== '.' && dir !== './') { - log('error', 'directory ' + opts._[0] + ' already exists') - process.exit(1) - } - } - if (dir === undefined) { - log('error', 'must specify a directory to \'fastify generate\'') - process.exit(1) - } - if (!opts.integrate && existsSync(path.join(dir, 'package.json'))) { - log('error', 'a package.json file already exists in target directory') - process.exit(1) - } - - generate(dir, pluginTemplate).catch(function (err) { - if (err) { - log('error', err.message) - process.exit(1) - } - }) -} - -module.exports = { - generate, - cli, - pluginTemplate -} - -if (require.main === module) { - cli(process.argv.slice(2)) -} diff --git a/generate-readme.js b/generate-readme.js deleted file mode 100644 index b0c1bd57..00000000 --- a/generate-readme.js +++ /dev/null @@ -1,138 +0,0 @@ -'use strict' - -const { readFileSync, existsSync } = require('fs') -const path = require('path') -const generify = require('generify') -const argv = require('yargs-parser') -const { execSync } = require('child_process') -const log = require('./log') - -function toMarkdownList (a) { - return a.map(d => `- ${d}`).join('\n') -} -function generate (dir, { pluginMeta, encapsulated, pluginFileName }) { - process.chdir(dir) - return new Promise((resolve, reject) => { - if (!existsSync(path.join(dir, 'package.json'))) { - execSync('npm init -y') - log('info', `generated package.json in ${dir}`) - } - - log('info', `reading package.json in ${dir}`) - let pkg = readFileSync('package.json') - try { - pkg = JSON.parse(pkg) - } catch (err) { - return reject(err) - } - - pluginMeta.decorators = pluginMeta.decorators ? pluginMeta.decorators : { fastify: [], reply: [] } - pluginMeta.dependencies = pluginMeta.dependencies ? pluginMeta.dependencies : [] - - const peerDepFastify = pkg.peerDependencies ? pkg.peerDependencies.fastify : '' - const depFastify = pkg.dependencies ? pkg.dependencies.fastify : '' - const minFastify = pluginMeta.fastify || peerDepFastify || depFastify - - let accessibilityTemplate = '' - if (!encapsulated) { - accessibilityTemplate = '- [X] Accessible in the same context where you require them\n- [ ] Accessible only in a child context\n' - } else { - accessibilityTemplate = '- [ ] Accessible in the same context where you require them\n- [X] Accessible only in a child context\n' - } - - const fastifyDecorators = toMarkdownList(pluginMeta.decorators.fastify) - const replyDecorators = toMarkdownList(pluginMeta.decorators.reply) - const pluginDeps = toMarkdownList(pluginMeta.dependencies) - - generify( - path.join(__dirname, 'templates', 'readme'), - dir, - { - accessibilityTemplate, - fastifyDecorators, - replyDecorators, - pluginDeps, - packageName: pkg.name, - pluginFileName, - minFastify - }, - function (file) { - log('debug', `generated ${file}`) - }, - function (err) { - if (err) { - return reject(err) - } - log('info', `README for plugin ${pkg.name} generated successfully`) - resolve() - } - ) - }) -} - -function stop (error) { - if (error) { - console.log(error) - process.exit(1) - } - process.exit() -} - -function showHelp () { - console.log( - readFileSync(path.join(__dirname, 'help', 'readme.txt'), 'utf8') - ) - return stop() -} - -function cli (args) { - const opts = argv(args) - - const dir = process.cwd() - - if (opts._.length !== 1) { - log('error', 'Missing the required file parameter\n') - return showHelp() - } - - if (existsSync(path.join(dir, 'README.md'))) { - log('error', 'a README.md file already exists in target directory') - process.exit(1) - } - - const pluginPath = path.join(dir, path.basename(opts._[0], '.js')) - - let plugin - try { - plugin = require(pluginPath) - } catch (err) { - log('error', 'plugin could not be loaded', err) - process.exit(1) - } - - const pluginMeta = plugin[Symbol.for('plugin-meta')] - - if (!pluginMeta) { - log('error', 'no plugin metadata could be found. Are you sure that you use https://github.com/fastify/fastify-plugin ?') - process.exit(1) - } - - const encapsulated = !plugin[Symbol.for('skip-override')] - const pluginFileName = path.basename(opts._[0]) - - generate(dir, { pluginMeta, encapsulated, pluginFileName }).catch(function (err) { - if (err) { - log('error', err.message) - process.exit(1) - } - }) -} - -module.exports = { - generate, - cli -} - -if (require.main === module) { - cli(process.argv.slice(2)) -} diff --git a/generate.js b/generate.js deleted file mode 100755 index c97f1aea..00000000 --- a/generate.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict' - -const { - readFile, - writeFile, - existsSync -} = require('fs') -const path = require('path') -const chalk = require('chalk') -const generify = require('generify') -const argv = require('yargs-parser') -const cliPkg = require('./package') -const { execSync } = require('child_process') -const log = require('./log') - -const javascriptTemplate = { - dir: 'app', - main: 'app.js', - scripts: { - test: 'tap "test/**/*.test.js"', - start: 'fastify start -l info app.js', - dev: 'fastify start -w -l info -P app.js' - }, - dependencies: { - fastify: cliPkg.dependencies.fastify, - 'fastify-plugin': cliPkg.devDependencies['fastify-plugin'] || cliPkg.dependencies['fastify-plugin'], - '@fastify/autoload': cliPkg.devDependencies['@fastify/autoload'], - '@fastify/sensible': cliPkg.devDependencies['@fastify/sensible'], - 'fastify-cli': '^' + cliPkg.version - }, - devDependencies: { - tap: cliPkg.devDependencies.tap - }, - logInstructions: function (pkg) { - log('debug', 'saved package.json') - log('info', `project ${pkg.name} generated successfully`) - log('debug', `run '${chalk.bold('npm install')}' to install the dependencies`) - log('debug', `run '${chalk.bold('npm start')}' to start the application`) - log('debug', `run '${chalk.bold('npm run dev')}' to start the application with pino-colada pretty logging (not suitable for production)`) - log('debug', `run '${chalk.bold('npm test')}' to execute the unit tests`) - } -} - -const typescriptTemplate = { - dir: 'app-ts', - main: 'app.ts', - scripts: { - test: 'npm run build:ts && tsc -p test/tsconfig.json && tap --ts test/**/*.test.ts', - start: 'npm run build:ts && fastify start -l info dist/app.js', - 'build:ts': 'tsc', - 'watch:ts': 'tsc -w', - dev: 'npm run build:ts && concurrently -k -p "[{name}]" -n "TypeScript,App" -c "yellow.bold,cyan.bold" "npm:watch:ts" "npm:dev:start"', - 'dev:start': 'fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js' - }, - dependencies: { - fastify: cliPkg.dependencies.fastify, - 'fastify-plugin': cliPkg.devDependencies['fastify-plugin'] || cliPkg.dependencies['fastify-plugin'], - '@fastify/autoload': cliPkg.devDependencies['@fastify/autoload'], - '@fastify/sensible': cliPkg.devDependencies['@fastify/sensible'], - 'fastify-cli': '^' + cliPkg.version - }, - devDependencies: { - '@types/node': cliPkg.devDependencies['@types/node'], - '@types/tap': cliPkg.devDependencies['@types/tap'], - 'ts-node': cliPkg.devDependencies['ts-node'], - concurrently: cliPkg.devDependencies.concurrently, - 'fastify-tsconfig': cliPkg.devDependencies['fastify-tsconfig'], - tap: cliPkg.devDependencies.tap, - typescript: cliPkg.devDependencies.typescript - }, - nodemonConfig: { - watch: ['src/'], - ignore: ['dist/*'] - }, - logInstructions: function (pkg) { - log('debug', 'saved package.json') - log('info', `project ${pkg.name} generated successfully`) - log('debug', `run '${chalk.bold('npm install')}' to install the dependencies`) - log('debug', `run '${chalk.bold('npm start')}' to start the application`) - log('debug', `run '${chalk.bold('npm build:ts')}' to compile the typescript application`) - log('debug', `run '${chalk.bold('npm run dev')}' to start the application with pino-colada pretty logging (not suitable for production)`) - log('debug', `run '${chalk.bold('npm test')}' to execute the unit tests`) - } -} - -function generate (dir, template) { - return new Promise((resolve, reject) => { - generify(path.join(__dirname, 'templates', template.dir), dir, {}, function (file) { - log('debug', `generated ${file}`) - }, function (err) { - if (err) { - return reject(err) - } - - process.chdir(dir) - execSync('npm init -y') - - log('info', `reading package.json in ${dir}`) - readFile('package.json', (err, data) => { - if (err) { - return reject(err) - } - - let pkg - try { - pkg = JSON.parse(data) - } catch (err) { - return reject(err) - } - - pkg.main = template.main - - pkg.scripts = Object.assign(pkg.scripts || {}, template.scripts) - - pkg.dependencies = Object.assign(pkg.dependencies || {}, template.dependencies) - - pkg.devDependencies = Object.assign(pkg.devDependencies || {}, template.devDependencies) - - log('debug', 'edited package.json, saving') - writeFile('package.json', JSON.stringify(pkg, null, 2), (err) => { - if (err) { - return reject(err) - } - - template.logInstructions(pkg) - resolve() - }) - }) - }) - }) -} - -function cli (args) { - const opts = argv(args) - const dir = opts._[0] - - if (dir && existsSync(dir)) { - if (dir !== '.' && dir !== './') { - log('error', 'directory ' + opts._[0] + ' already exists') - process.exit(1) - } - } - if (dir === undefined) { - log('error', 'must specify a directory to \'fastify generate\'') - process.exit(1) - } - if (!opts.integrate && existsSync(path.join(dir, 'package.json'))) { - log('error', 'a package.json file already exists in target directory') - process.exit(1) - } - - const template = opts.lang === 'ts' || opts.lang === 'typescript' ? typescriptTemplate : javascriptTemplate - - generate(dir, template).catch(function (err) { - if (err) { - log('error', err.message) - process.exit(1) - } - }) -} - -module.exports = { - generate, - cli, - javascriptTemplate, - typescriptTemplate -} - -if (require.main === module) { - cli(process.argv.slice(2)) -} diff --git a/help/eject-ts.txt b/help/eject-ts.txt deleted file mode 100644 index 98562b76..00000000 --- a/help/eject-ts.txt +++ /dev/null @@ -1,5 +0,0 @@ -Usage: fastify eject-ts - -Turns your application into a standalone executable with a server.ts file being added. - -You can set the port to listen on with the PORT environment variable (default to 3000). \ No newline at end of file diff --git a/help/eject.txt b/help/eject.txt deleted file mode 100644 index adb5289f..00000000 --- a/help/eject.txt +++ /dev/null @@ -1,5 +0,0 @@ -Usage: fastify eject - -Turns your application into a standalone executable with a server.js file being added. - -You can set the port to listen on with the PORT environment variable (default to 3000). \ No newline at end of file diff --git a/help/generate-plugin.txt b/help/generate-plugin.txt deleted file mode 100644 index acef1b87..00000000 --- a/help/generate-plugin.txt +++ /dev/null @@ -1,5 +0,0 @@ -Usage: fastify generate-plugin - -Sets up plugin project with `npm init -y` and -generates a sample Fastify plugin project -in the . Specify `.` to create files in the current working directory. diff --git a/help/generate.txt b/help/generate.txt deleted file mode 100644 index f2a111ad..00000000 --- a/help/generate.txt +++ /dev/null @@ -1,13 +0,0 @@ -Usage: fastify generate - -Sets up project with `npm init -y` and -generates a sample Fastify project -in the . Specify `.` to create files in the current working directory. - -OPTS - - --integrate - overwrite it when the target directory is the current directory (.) - - --lang=ts, --lang=typescript - use the TypeScript template diff --git a/help/help.txt b/help/help.txt deleted file mode 100644 index 6fc6a2d3..00000000 --- a/help/help.txt +++ /dev/null @@ -1,13 +0,0 @@ -Fastify command line interface available commands are: - - * start start a server - * eject turns your application into a standalone executable with a server.js file being added. - * eject-ts turns your application into a standalone executable with a server.ts file being added. - * generate generate a new project - * generate-plugin generate a new plugin project - * readme generate a README.md for the plugin - * print-routes prints the representation of the internal radix tree used by the router, useful for debugging. - * version the current fastify-cli version - * help help about commands - -Launch 'fastify help [command]' to learn more about each command. diff --git a/help/print-routes.txt b/help/print-routes.txt deleted file mode 100644 index 782e5065..00000000 --- a/help/print-routes.txt +++ /dev/null @@ -1 +0,0 @@ -Usage: fastify print-routes diff --git a/help/readme.txt b/help/readme.txt deleted file mode 100644 index 6e2fe972..00000000 --- a/help/readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -Usage: fastify readme - -Sets up project with `npm init -y` and -generates a sample README.md file -from the . \ No newline at end of file diff --git a/help/start.txt b/help/start.txt deleted file mode 100644 index 0b64fd36..00000000 --- a/help/start.txt +++ /dev/null @@ -1,63 +0,0 @@ -Usage: fastify start [opts] [--] [] - -OPTS - - -p, --port - [env: FASTIFY_PORT or PORT] - Port to listen on (default to 3000) - - -a, --address - [env: FASTIFY_ADDRESS] - Address to listen on - - -s, --socket - [env: FASTIFY_SOCKET] - Socket to listen on - - -l, --log-level - [env: FASTIFY_LOG_LEVEL] - Log level (default to fatal) - - -r, --require - [env: FASTIFY_REQUIRE] - Module to preload - - -L, --logging-module - [env: FASTIFY_LOGGING_MODULE] - Path to logging configuration module to use - - -P, --pretty-logs - [env: FASTIFY_PRETTY_LOGS] - Prints pretty logs - - -o, --options - [env: FASTIFY_OPTIONS] - Use custom options - - -w, --watch - [env: FASTIFY_WATCH] - Watch process.cwd() directory for changes, recursively; when that happens, the process will auto reload. - - -x, --prefix - [env: FASTIFY_PREFIX] - Set the prefix - - --body-limit - [env: FASTIFY_BODY_LIMIT] - Defines the maximum payload, in bytes, the server is allowed to accept - - -T, --plugin-timeout - The maximum amount of time that a plugin can take to load (default to 10 seconds). - - -h, --help - Show this help message - -Examples: - - start plugin.js on port 8080 - - fastify start -p 8080 plugin.js - - start plugin.js passing custom options to it - - fastify start plugin.js -- --custom-plugin-option-1 --custom-plugin-option-2 diff --git a/helper.js b/helper.js deleted file mode 100644 index 8950372b..00000000 --- a/helper.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -const { runFastify } = require('./start') - -module.exports = { - build (args, additionalOptions = {}) { - Object.defineProperty(additionalOptions, 'ready', { - value: true, - enumerable: false, - writable: false - }) - return runFastify(args, additionalOptions) - }, - listen: runFastify -} diff --git a/lib/watch/constants.js b/lib/watch/constants.js deleted file mode 100644 index 742c31bc..00000000 --- a/lib/watch/constants.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports = { - GRACEFUL_SHUT: 'GRACEFUL SHUTDOWN', - READY: 'ready', - TIMEOUT: 5000 -} diff --git a/lib/watch/fork.js b/lib/watch/fork.js deleted file mode 100644 index d4ee7922..00000000 --- a/lib/watch/fork.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict' - -const chalk = require('chalk') -const { stop, runFastify } = require('../../start') - -const { - GRACEFUL_SHUT, - READY, - TIMEOUT -} = require('./constants.js') - -let fastify - -function exit () { - if (this) { console.log(chalk.red(this.message)) } - process.exit(1) -} - -process.on('message', function (event) { - if (event === GRACEFUL_SHUT) { - const message = chalk.red('[fastify-cli] process forced end') - setTimeout(exit.bind({ message }), TIMEOUT).unref() - if (fastify) { - fastify.close(() => { - process.exit(0) - }) - } else { - process.exit(0) - } - } -}) - -process.on('uncaughtException', (err) => { - console.log(chalk.red(err)) - const message = chalk.red('[fastify-cli] app crashed - waiting for file changes before starting...') - exit.bind({ message })() -}) - -const main = async () => { - fastify = await runFastify(process.argv.splice(2)) - const type = process.env.childEvent - - process.send({ type, err: null }) - - try { - await fastify.ready() - process.send({ type: READY }) - } catch (err) { - stop(err) - } -} - -main().catch((err) => { - console.error(err) - process.exit(1) -}) diff --git a/lib/watch/index.js b/lib/watch/index.js deleted file mode 100644 index adf651bc..00000000 --- a/lib/watch/index.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict' - -const path = require('path') -const cp = require('child_process') -const chalk = require('chalk') -const { arrayToRegExp, logWatchVerbose } = require('./utils') -const { GRACEFUL_SHUT } = require('./constants.js') - -const EventEmitter = require('events') -const chokidar = require('chokidar') -const forkPath = path.join(__dirname, './fork.js') - -const watch = function (args, ignoreWatch, verboseWatch) { - const emitter = new EventEmitter() - let allStop = false - let childs = [] - - const stop = (watcher = null, err = null) => { - childs.forEach(function (child) { - child.kill() - }) - - childs = [] - if (err) { - console.log(chalk.red(err)) - } - if (watcher) { - allStop = true - return watcher.close() - } - } - - process.on('uncaughtException', () => { - stop() - childs.push(run('restart')) - }) - - let readyEmitted = false - - const run = (event) => { - const childEvent = { childEvent: event } - const env = Object.assign({}, process.env, childEvent, require('dotenv').config().parsed) - - const _child = cp.fork(forkPath, args, { - env, - cwd: process.cwd(), - encoding: 'utf8' - }) - - _child.on('exit', function (code, signal) { - if (!code === 0) { - stop() - } - if (childs.length === 0 && !allStop) { - childs.push(run('restart')) - } - return null - }) - - _child.on('message', (event) => { - const { type, err } = event - if (err) { - emitter.emit('error', err) - return null - } - - if (type === 'ready') { - if (readyEmitted) { - return - } - - readyEmitted = true - } - - emitter.emit(type, err) - }) - - return _child - } - - childs.push(run('start')) - const ignoredArr = ignoreWatch.split(' ').map((item) => item.trim()).filter((item) => item.length) - const ignoredPattern = arrayToRegExp(ignoredArr) - - const watcher = chokidar.watch(process.cwd(), { ignored: ignoredPattern }) - watcher.on('ready', function () { - watcher.on('all', function (event, filepath) { - if (verboseWatch) { - logWatchVerbose(event, filepath) - } - try { - const child = childs.shift() - child.send(GRACEFUL_SHUT) - } catch (err) { - if (childs.length !== 0) { - console.log(chalk.red(err)) - stop(watcher, err) - } - childs.push(run('restart')) - } - }) - }) - - emitter.on('error', (err) => { - stop(watcher, err) - }) - - emitter.on('close', () => { - stop(watcher) - }) - - emitter.stop = stop.bind(null, watcher) - - return emitter -} - -module.exports = watch diff --git a/lib/watch/utils.js b/lib/watch/utils.js deleted file mode 100644 index 894a2626..00000000 --- a/lib/watch/utils.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const chalk = require('chalk') -const path = require('path') - -const arrayToRegExp = (arr) => { - const reg = arr.map((file) => { - if (/^\./.test(file)) { return `\\${file}` } - return file - }).join('|') - return new RegExp(`(${reg})`) -} - -const logWatchVerbose = (event, filepath) => { - const relativeFilepath = path.relative(process.cwd(), filepath) - console.log( - chalk.gray( - `[fastify-cli] watch - '${event}' occurred on '${relativeFilepath}'` - ) - ) -} - -module.exports = { - arrayToRegExp, - logWatchVerbose -} diff --git a/log.js b/log.js deleted file mode 100644 index 84987b9a..00000000 --- a/log.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -const chalk = require('chalk') - -const levels = { - debug: 0, - info: 1, - error: 2 -} - -const colors = [l => l, chalk.green, chalk.red] - -function log (severity, line) { - const level = levels[severity] || 0 - if (level === 1) { - line = '--> ' + line - } - console.log(colors[level](line)) -} - -module.exports = log diff --git a/package.json b/package.json index 4a9001d7..2cac36ee 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,28 @@ { "name": "fastify-cli", - "version": "4.0.0", + "version": "3.1.0", "description": "Run a fastify route with one command!", - "main": "cli.js", + "main": "dist/index.js", "bin": { - "fastify": "cli.js" + "fastify": "./bin/run" }, + "files": [ + "/bin", + "/dist", + "/templates", + "/oclif.manifest.json" + ], "scripts": { - "lint": "standard", - "lint:fix": "standard --fix", - "unit": "tap \"test/**/*.test.{js,ts}\" \"templates/**/*.test.{js,ts}\"", - "pretest": "xcopy /e /k /i . \"..\\node_modules\\fastify-cli\" || rsync -r --exclude=node_modules ./ node_modules/fastify-cli || echo 'this is fine'", - "test": "npm run lint && npm run unit && npm run test:typescript", - "test:typescript": "tsd templates/plugin && tsc --project templates/app-ts/tsconfig.json && del-cli templates/app-ts/dist" + "build": "shx rm -rf dist && tsc -p tsconfig.json", + "lint": "eslint . --ext .ts", + "lint:fix": "npm run lint --fix", + "prepack": "npm run build", + "pack": "oclif manifest && oclif readme", + "postpack": "shx rm -f oclif.manifest.json", + "preunit": "npm run pack", + "unit": "tap \"test/**/*.test.ts\"", + "test": "npm run lint && npm run unit", + "snap": "TAP_SNAPSHOT=1 && npm run unit" }, "keywords": [ "fastify", @@ -36,58 +46,74 @@ }, "homepage": "https://github.com/fastify/fastify-cli#readme", "engines": { - "node": ">= 10" - }, - "standard": { - "ignore": [ - "test/data/parsing-error.js", - "test/data/undefinedVariable.js" - ] + "node": ">= 12" }, "dependencies": { - "chalk": "^4.1.2", - "chokidar": "^3.5.2", - "close-with-grace": "^1.1.0", - "commist": "^2.0.0", - "dotenv": "^16.0.0", + "@fastify/autoload": "^5.0.0", + "@fastify/sensible": "^5.0.0", + "@oclif/core": "^1.8.2", + "@oclif/plugin-help": "^5.1.12", + "ansi-colors": "^4.1.3", + "chokidar": "^3.5.3", + "ejs": "^3.1.8", "fastify": "^4.0.0-rc.2", - "generify": "^4.0.0", - "help-me": "^4.0.0", - "is-docker": "^2.0.0", - "make-promises-safe": "^5.1.0", - "pino-colada": "^2.2.2", - "pkg-up": "^3.1.0", - "pump": "^3.0.0", + "fastify-plugin": "^3.0.0", + "inquirer": "^8.2.4", + "is-docker": "^3.0.0", + "pkg-up": "^4.0.0", "resolve-from": "^5.0.0", - "semver": "^7.3.5", - "split2": "^4.1.0", - "yargs-parser": "^21.0.1" + "semver": "^7.3.7", + "tslib": "^2.4.0" }, "devDependencies": { - "@fastify/autoload": "^5.0.0", - "@fastify/sensible": "^4.1.0", - "@types/node": "^17.0.8", - "@types/tap": "^15.0.5", - "concurrently": "^7.0.0", - "del-cli": "^4.0.1", - "fastify-plugin": "^3.0.0", - "fastify-tsconfig": "^1.0.1", - "minimatch": "^5.1.0", - "pre-commit": "^1.2.2", - "proxyquire": "^2.1.3", - "rimraf": "^3.0.2", - "simple-get": "^4.0.0", - "sinon": "^14.0.0", - "standard": "^17.0.0", - "strip-ansi": "^6.0.1", - "tap": "^16.1.0", - "ts-node": "^10.4.0", - "ts-standard": "^11.0.0", - "tsd": "^0.16.0", - "typescript": "^4.5.4", - "walker": "^1.0.8" + "@types/ejs": "^3.1.1", + "@types/inquirer": "^8.2.1", + "@types/node": "^17.0.0", + "@types/semver": "^7.3.9", + "@types/tap": "^15.0.0", + "@typescript-eslint/eslint-plugin": "^4.0.1", + "eslint": "^7.12.1", + "eslint-config-standard-with-typescript": "^21.0.1", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.0.0", + "oclif": "^3.0.1", + "pino-pretty": "^7.6.1", + "shx": "^0.3.4", + "tap": "^16.2.0", + "ts-node": "^10.7.0", + "typescript": "~4.4.0" + }, + "oclif": { + "bin": "fastify", + "dirname": "fastify", + "commands": "./dist/commands", + "plugins": [ + "@oclif/plugin-help" + ], + "topicSeparator": " ", + "topics": { + "generate": { + "description": "Generate boilerplate" + } + } }, - "tsd": { - "directory": "test" + "eslintConfig": { + "extends": "standard-with-typescript", + "parserOptions": { + "project": "./tsconfig.eslint.json" + }, + "overrides": [ + { + "files": [ + "templates/**/*.test.ts", + "test/**/*.test.ts" + ], + "rules": { + "@typescript-eslint/no-floating-promises": "off" + } + } + ], + "ignorePatterns": "src/utils/command/*.ts" } } diff --git a/print-routes.js b/print-routes.js deleted file mode 100644 index a7e235ba..00000000 --- a/print-routes.js +++ /dev/null @@ -1,82 +0,0 @@ -#! /usr/bin/env node - -'use strict' - -const parseArgs = require('./args') -const log = require('./log') -const { - exit, - requireFastifyForModule, - requireServerPluginFromPath, - showHelpForCommand -} = require('./util') - -let Fastify = null - -function loadModules (opts) { - try { - Fastify = requireFastifyForModule(opts._[0]).module - } catch (e) { - module.exports.stop(e) - } -} - -function printRoutes (args) { - const opts = parseArgs(args) - if (opts.help) { - return showHelpForCommand('print-routes') - } - - if (opts._.length !== 1) { - console.error('Missing the required file parameter\n') - return showHelpForCommand('print-routes') - } - - // we start crashing on unhandledRejection - require('make-promises-safe') - - loadModules(opts) - - return runFastify(opts) -} - -async function runFastify (opts) { - require('dotenv').config() - - let file = null - - try { - file = await requireServerPluginFromPath(opts._[0]) - } catch (e) { - return module.exports.stop(e) - } - - const fastify = Fastify(opts.options) - - const pluginOptions = {} - if (opts.prefix) { - pluginOptions.prefix = opts.prefix - } - - await fastify.register(file, pluginOptions) - await fastify.ready() - log('debug', fastify.printRoutes()) - - return fastify -} - -function stop (message) { - exit(message) -} - -function cli (args) { - return printRoutes(args).then(fastify => { - if (fastify) return fastify.close() - }) -} - -module.exports = { cli, stop, printRoutes } - -if (require.main === module) { - cli(process.argv.slice(2)) -} diff --git a/src/commands/generate/project.ts b/src/commands/generate/project.ts new file mode 100644 index 00000000..cc0d40fb --- /dev/null +++ b/src/commands/generate/project.ts @@ -0,0 +1,246 @@ +import { Flags } from '@oclif/core' +import { execSync } from 'child_process' +import { compile } from 'ejs' +import { access, mkdir, readFile, rm, stat, writeFile } from 'fs/promises' +import { prompt } from 'inquirer' +import { basename, dirname, join, resolve } from 'path' +import { Command } from '../../utils/command/command' +import { computePackageJSON } from '../../utils/package-json' + +export default class Project extends Command { + static description = 'Generate fastify project' + + static args = [ + { name: 'name', required: false, description: 'Name of the project.' } + ] + + static flags = { + location: Flags.string({ description: 'Location to place the project.' }), + overwrite: Flags.boolean({ description: 'Force to overwrite the project location when it exist.', default: false }), + language: Flags.string({ description: 'Programming Language you would like to use in this project.' }), + lint: Flags.string({ description: 'Lint Tools you would like to use in this project.' }), + test: Flags.string({ description: 'Test Framework you would like to use in this project.' }), + help: Flags.help() + } + + shouldOverwrite = false + + async run (): Promise { + const { args, flags } = await this.parse(Project) + + // validate + if (flags.language !== undefined) this.corceLanguage(flags.language) + + const answer: any = {} + + Object.assign(answer, await prompt([ + { type: 'input', name: 'name', message: 'What is your project name?', validate: this.questionNameValidate }, + { type: 'input', name: 'location', message: 'Where do you want to place your project?', default: this.questionLocationDefault }, + { type: 'confirm', name: 'overwrite', message: 'The folder already exist. Do you want to overwrite?', default: flags.overwrite ?? false, when: this.questionOverwriteWhen, askAnswered: true } + ], { + name: args.name, + location: flags.location, + overwrite: flags.overwrite + })) + + if (this.shouldOverwrite) this.questionOverwriteValidate(answer.overwrite) + + Object.assign(answer, await prompt([ + { type: 'list', name: 'language', message: 'Which language will you use?', default: 'JavaScript', choices: ['JavaScript', 'TypeScript'] }, + { type: 'list', name: 'lint', message: 'Which linter would you like to use?', default: this.questionLintDefault, choices: this.questionLintChoices }, + { type: 'list', name: 'test', message: 'Which test framework would you like to use?', default: 'tap', choices: ['tap'] } + ], { + language: flags.language, + lint: flags.lint, + test: flags.test + })) + + const fileList = await this.computeFileList(answer) + const files = await this.prepareFiles(fileList, answer) + await this.writeFiles(files, answer) + await this.npmInstall(answer) + this.log(`project "${answer.name as string}" initialized in "${answer.location as string}"`) + } + + questionNameValidate = (input: string): true | string => { + if (String(input).trim() === '') { + return 'Project Name cannot be empty.' + } + return true + } + + questionLocationDefault = (answer: any): string => { + return this.toLocation(answer.name) + } + + questionOverwriteWhen = async (answer: any): Promise => { + this.shouldOverwrite = !(await this.validateProjectLocation(answer.location)) + return this.shouldOverwrite + } + + questionOverwriteValidate = (input: boolean): true | undefined => { + if (input) return true + this.log('Terminated because location cannot be overwrite.') + this.exit(0) + } + + questionLintDefault =(answer: any): string => { + switch (this.corceLanguage(answer.language)) { + case 'JavaScript': + return 'standard' + case 'TypeScript': + return 'ts-standard' + } + } + + questionLintChoices = (answer: any): string[] => { + switch (this.corceLanguage(answer.language)) { + case 'JavaScript': + return ['standard', 'eslint', 'eslint + standard'] + case 'TypeScript': + return ['ts-standard', 'eslint', 'eslint + ts-standard'] + } + } + + corceLanguage (str: string): 'JavaScript' | 'TypeScript' { + switch (str.trim().toLowerCase()) { + case 'js': + case 'javascript': + return 'JavaScript' + case 'ts': + case 'typescript': + return 'TypeScript' + default: + throw new Error(`Programming Language expected to be "JavaScript" or "TypeScript", but recieved "${str}"`) + } + } + + toLocation (name: string): string { + return name.trim().toLowerCase().replace(/\s+/g, '-') + } + + async isFileExist (path: string): Promise { + try { + const stats = await stat(path) + return stats.isFile() + } catch { + return false + } + } + + async validateProjectLocation (location: string): Promise { + const path = resolve(location) + try { + await access(path) + return false + } catch { + return true + } + } + + computeFileList (answer: any): string[] { + // we do not add .ejs in here + // we should find the file if .ejs exist first and then compile to the destination + const files: string[] = [ + '.vscode/settings.json', + 'README.md', + '__gitignore' + ] + + if (answer.language === 'JavaScript') { + files.push('app.js') + files.push('plugins/README.md') + files.push('plugins/sensible.js') + files.push('plugins/support.js') + files.push('routes/README.md') + files.push('routes/root.js') + files.push('routes/example/index.js') + files.push('test/helper.js') + files.push('test/plugins/support.test.js') + files.push('test/routes/root.test.js') + files.push('test/routes/example.test.js') + } + + if (answer.language === 'TypeScript') { + files.push('tsconfig.json') + files.push('tsconfig.build.json') + files.push('src/app.ts') + files.push('src/plugins/README.md') + files.push('src/plugins/sensible.ts') + files.push('src/plugins/support.ts') + files.push('src/routes/README.md') + files.push('src/routes/root.ts') + files.push('src/routes/example/index.ts') + files.push('test/helper.ts') + files.push('test/plugins/support.test.ts') + files.push('test/routes/root.test.ts') + files.push('test/routes/example.test.ts') + } + + return files + } + + async resolveFile (file: string): Promise<{ template: boolean, content: string }> { + const o = { template: false, content: '' } + const ejsPath = resolve(join('templates', 'project', `${file}.ejs`)) + const isEJSTemplate = await this.isFileExist(ejsPath) + if (isEJSTemplate) { + o.template = true + const data = await readFile(ejsPath) + o.content = data.toString() + return o + } + const filePath = resolve(join('templates', 'project', file)) + const isFileExist = await this.isFileExist(filePath) + if (isFileExist) { + const data = await readFile(filePath) + o.content = data.toString() + return o + } + throw new Error(`File ${file} is missing, please check if the module installed properly.`) + } + + async prepareFiles (files: string[], answer: any): Promise<{ [path: string]: string }> { + const o: { [path: string]: string } = {} + o['package.json'] = computePackageJSON(answer) + for (let file of files) { + const { template, content } = await this.resolveFile(file) + const dir = dirname(file) + file = `${dir}/${basename(file).replace('__', '.')}` + if (template) { + const render = compile(content, { async: true }) + o[file] = await render(answer) + } else { + o[file] = content + } + } + return o + } + + async writeFiles (files: { [path: string]: string }, answer: any): Promise { + if (this.shouldOverwrite) { + this.log(`remove folder "${answer.location as string}"`) + await rm(resolve(answer.location as string), { recursive: true, force: true }) + } + for (const [path, content] of Object.entries(files)) { + const realpath = join(answer.location, path) + const fullpath = resolve(realpath) + await mkdir(dirname(fullpath), { recursive: true }) + await writeFile(fullpath, content) + this.log(`write file "${path}" to "${realpath}"`) + } + } + + async npmInstall (answer: any): Promise { + this.log('run "npm install"') + const result: any = await prompt([ + { type: 'confirm', name: 'npm', message: 'Do you want to run "npm install"?', default: true } + ]) + if (result.npm === true) { + execSync('npm install', { + cwd: resolve(answer.location), + stdio: 'inherit' + }) + } + } +} diff --git a/src/commands/start.ts b/src/commands/start.ts new file mode 100644 index 00000000..8f4d7ecd --- /dev/null +++ b/src/commands/start.ts @@ -0,0 +1,53 @@ +import { Flags } from '@oclif/core' +import { Command } from '../utils/command/command' +import { start, StartOption } from '../utils/start' +import { watch } from '../utils/watch/watch' + +export default class Start extends Command { + static description = 'Start fastify instance' + + static args = [ + { name: 'entry', required: true, description: 'Entry point of fastify instance.' } + ] + + static flags = { + require: Flags.string({ char: 'r', description: 'Preload Modules, for example "-r ts-node/register".', multiple: true }), + port: Flags.integer({ char: 'p', description: '[default: 3000] Port listen on.' }), + address: Flags.string({ char: 'a', description: '[default: localhost] Address listen on. It can be either address or socket.' }), + debug: Flags.boolean({ char: 'd', description: '[default: false] Enable debug mode.' }), + 'debug-port': Flags.integer({ description: '[default: 9320] Inspector port.', dependsOn: ['debug'] }), + 'debug-address': Flags.string({ description: 'Inspector host, by default it will be either "localhost" or "0.0.0.0" in docker.', dependsOn: ['debug'] }), + prefix: Flags.string({ description: '[default: ""] Entry file prefix.' }), + 'pretty-logs': Flags.boolean({ char: 'P', description: '[default: false] Use "pino-pretty" for log display. It require to install the module seperately.' }), + watch: Flags.boolean(), + 'watch-ignore': Flags.string(), + 'watch-verbose': Flags.boolean(), + help: Flags.help() + } + + async run (): Promise { + const { args, flags } = await this.parse(Start) + + // we normalize the options before start + const options: Partial = { + prefix: flags.prefix, + entry: args.entry, + require: flags.require, + port: flags.port, + address: flags.address, + debug: flags.debug, + debugPort: flags['debug-port'], + debugAddress: flags['debug-address'], + pretty: flags['pretty-logs'], + watch: flags.watch, + watchIgnorePattern: flags['watch-ignore'], + watchVerbose: flags['watch-verbose'] + } + + if (options.watch === true) { + await watch(options) + } else { + await start(options) + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..0237950a --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export { run } from '@oclif/core' diff --git a/src/utils/command/command.ts b/src/utils/command/command.ts new file mode 100644 index 00000000..a31d1b40 --- /dev/null +++ b/src/utils/command/command.ts @@ -0,0 +1,14 @@ +// This file actually address the issue from +// https://github.com/oclif/oclif/issues/190 +import { Command as RawCommand, Interfaces } from '@oclif/core' +import * as Parser from './parser' + +export abstract class Command extends RawCommand { + protected async parse(options?: Interfaces.Input, argv = this.argv): Promise> { + if (options == null) options = this.constructor as any + const opts = { context: this, ...options } + // the spread operator doesn't work with getters so we have to manually add it here + opts.flags = options?.flags + return await Parser.parse(argv, opts) + } +} diff --git a/src/utils/command/parser.ts b/src/utils/command/parser.ts new file mode 100644 index 00000000..a51e88cc --- /dev/null +++ b/src/utils/command/parser.ts @@ -0,0 +1,149 @@ +// This file actually address the issue from +// https://github.com/oclif/oclif/issues/190 +import { Input, OutputArgs, OutputFlags, ParserInput, ParserOutput } from '@oclif/core/lib/interfaces' +import * as args from '@oclif/core/lib/parser/args' +import Deps from '@oclif/core/lib/parser/deps' +import * as flags from '@oclif/core/lib/parser/flags' +import { Parser as RawParser } from '@oclif/core/lib/parser/parse' + +export class Parser, TArgs extends OutputArgs> extends RawParser { + + async parse (): Promise { + // @ts-expect-error + this._debugInput() + + const findLongFlag = (arg: string): string | undefined => { + const name = arg.slice(2) + // @ts-expect-error + if (this.input.flags[name]) { + return name + } + + if (arg.startsWith('--no-')) { + // @ts-expect-error + const flag = this.booleanFlags[arg.slice(5)] + if (flag && flag.allowNo) return flag.name + } + } + + const findShortFlag = (arg: string): string[] => { + // @ts-expect-error + return Object.keys(this.input.flags).find(k => this.input.flags[k].char === arg[1]) + } + + const parseFlag = (arg: string): boolean => { + const long = arg.startsWith('--') + const name = long ? findLongFlag(arg) : findShortFlag(arg) + if (!name) { + const i = arg.indexOf('=') + if (i !== -1) { + const sliced = arg.slice(i + 1) + // @ts-expect-error + this.argv.unshift(sliced) + + const equalsParsed = parseFlag(arg.slice(0, i)) + if (!equalsParsed) { + // @ts-expect-error + this.argv.shift() + } + + return equalsParsed + } + + return false + } + + // @ts-expect-error + const flag = this.input.flags[name] + if (flag.type === 'option') { + // @ts-expect-error + this.currentFlag = flag + // @ts-expect-error + const input = long || arg.length < 3 ? this.argv.shift() : arg.slice(arg[2] === '=' ? 3 : 2) + if (typeof input !== 'string') { + // @ts-expect-error + throw new m.errors.CLIError(`Flag --${name} expects a value`) + } + + // @ts-expect-error + this.raw.push({ type: 'flag', flag: flag.name, input }) + } else { + // @ts-expect-error + this.raw.push({ type: 'flag', flag: flag.name, input: arg }) + // push the rest of the short characters back on the stack + if (!long && arg.length > 2) { + // @ts-expect-error + this.argv.unshift(`-${arg.slice(2)}`) + } + } + + return true + } + + let parsingFlags = true + // @ts-expect-error + while (this.argv.length > 0) { + // @ts-expect-error + const input = this.argv.shift() as string + if (parsingFlags && input.startsWith('-') && input !== '-') { + // attempt to parse as arg + // @ts-expect-error + if (this.input['--'] !== false && input === '--') { + parsingFlags = false + continue + } + + if (parseFlag(input)) { + continue + } + // not actually a flag if it reaches here so parse as an arg + } + + // not a flag, parse as arg + // @ts-expect-error + const arg = this.input.args[this._argTokens.length] + if (arg) arg.input = input + // @ts-expect-error + this.raw.push({ type: 'arg', input }) + } + + // @ts-expect-error + const argv = await this._argv() + // @ts-expect-error + const args = this._args(argv) + // @ts-expect-error + const flags = await this._flags() + // @ts-expect-error + this._debugOutput(argv, args, flags) + return { + args, + argv, + flags, + // @ts-expect-error + raw: this.raw, + // @ts-expect-error + metadata: this.metaData + } + } +} + +const m = Deps() + .add('validate', () => require('@oclif/core/lib/parser/validate').validate) + +export async function parse (argv: string[], options: Input): Promise> { + const input = { + argv, + context: options.context, + args: (options.args || []).map((a: any) => args.newArg(a as any)), + '--': options['--'], + flags: { + color: flags.defaultFlags.color, + ...((options.flags || {})) + }, + strict: options.strict !== false + } + const parser = new Parser(input) + const output = await parser.parse() + m.validate({ input, output }) + return output as ParserOutput +} diff --git a/src/utils/import.ts b/src/utils/import.ts new file mode 100644 index 00000000..da3303d8 --- /dev/null +++ b/src/utils/import.ts @@ -0,0 +1,12 @@ +const moduleCache = new Map() + +type ESMImport = (name: string) => Promise +// eslint-disable-next-line @typescript-eslint/no-implied-eval,no-new-func +export const _esmImport = new Function('modulePath', 'return import(modulePath)') as ESMImport + +export async function _import (name: string): Promise { + if (moduleCache.has(name)) return moduleCache.get(name) + const module = await _esmImport(name) + moduleCache.set(name, module) + return module +} diff --git a/src/utils/package-json.ts b/src/utils/package-json.ts new file mode 100644 index 00000000..f75ad4a5 --- /dev/null +++ b/src/utils/package-json.ts @@ -0,0 +1,192 @@ +import { resolve } from 'path' + +export function findPackageJSON (): { + version: string + dependencies: { [key: string]: string } + devDependencies: { [key: string]: string } +} { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require(resolve('package.json')) + return { + version: pkg.version, + dependencies: pkg.dependencies, + devDependencies: pkg.devDependencies + } +} + +export function computePackageJSON (answer: any): string { + const pkg = findPackageJSON() + const template: any = {} + template.name = answer.name + template.main = answer.language === 'JavaScript' ? 'app.js' : 'dist/app.js' + template.scripts = computeScripts(answer) + template.dependencies = computeDependencies(answer, pkg) + template.devDependencies = computeDevDependencies(answer, pkg) + if (String(answer.lint).includes('eslint')) { + template.eslintConfig = computeESLintConfig(template.devDependencies) + } + + return JSON.stringify(template, null, 2) +} + +function sort (dependencies: { [key: string]: string }): { [key: string]: string } { + const keys = Object.keys(dependencies).sort() + const obj: { [key: string]: string } = {} + for (const key of keys) { + obj[key] = dependencies[key] + } + return obj +} + +export function computeScripts (answer: any): { [key: string]: string } { + const scripts: any = {} + // Lint Command + if (answer.lint === 'standard') { + scripts.lint = 'standard' + } + if (answer.lint === 'ts-standard') { + scripts.lint = 'ts-standard' + } + if (String(answer.lint).includes('eslint')) { + scripts.lint = `eslint . ${answer.language === 'TypeScript' ? '--ext .ts' : ''}` + } + + // Other Commands + if (answer.language === 'JavaScript') { + scripts.test = 'tap "test/**/*.test.js"' + scripts.start = 'fastify start -l info app.js' + scripts.dev = 'fastify start -w -l info -P app.js' + } + if (answer.language === 'TypeScript') { + scripts.test = 'tap "test/**/*.test.ts" --ts' + scripts.start = 'fastify start -r ts-node/register -l info app.js' + scripts.dev = 'fastify start -r ts-node/register -w -l info -P app.js' + } + return scripts +} + +export function computeDependencies (_answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies['@fastify/autoload'] = pkg.dependencies['@fastify/autoload'] + dependencies['@fastify/sensible'] = pkg.dependencies['@fastify/sensible'] + dependencies.fastify = pkg.dependencies.fastify + dependencies['fastify-cli'] = pkg.version + dependencies['fastify-plugin'] = pkg.dependencies['fastify-plugin'] + return sort(dependencies) +} + +// ts-standard + +export function computeDevDependencies (answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies.tap = pkg.devDependencies.tap + if (answer.language === 'TypeScript') { + dependencies['@types/tap'] = pkg.devDependencies['@types/tap'] + dependencies['@types/node'] = pkg.devDependencies['@types/node'] + dependencies['ts-node'] = pkg.devDependencies['ts-node'] + dependencies.typescript = pkg.devDependencies.typescript + } + if (String(answer.lint).includes('eslint')) { + // shared + dependencies.eslint = '^8.16.0' + dependencies['eslint-plugin-import'] = pkg.devDependencies['eslint-plugin-import'] + dependencies['eslint-plugin-promise'] = pkg.devDependencies['eslint-plugin-promise'] + + // ts-standard + if (String(answer.lint).includes('ts-standard')) { + dependencies.eslint = pkg.devDependencies.eslint + dependencies['eslint-plugin-node'] = pkg.devDependencies['eslint-plugin-node'] + dependencies['eslint-config-standard-with-typescript'] = pkg.devDependencies['eslint-config-standard-with-typescript'] + // require >=3.3.1 <4.5.0 + dependencies.typescript = '~4.4.0' + } else + // standard + if (String(answer.lint).includes('standard')) { + dependencies['eslint-config-standard'] = '^17.0.0' + } + + // non ts-standard + if (!String(answer.lint).includes('ts-standard')) { + // eslint, eslint + standard + // override version + dependencies['eslint-plugin-promise'] = '^6.0.0' + // eslint, eslint + standard + dependencies['eslint-plugin-n'] = '^15.2.0' + } + + // TypeScript + if (answer.language === 'TypeScript') { + dependencies['@typescript-eslint/eslint-plugin'] = pkg.devDependencies['@typescript-eslint/eslint-plugin'] + if (!String(answer.lint).includes('ts-standard')) { + dependencies['@typescript-eslint/parser'] = '^5.25.0' + } + } + } + if (answer.lint === 'standard') { + dependencies.standard = '^17.0.0' + } + if (answer.lint === 'ts-standard') { + dependencies['ts-standard'] = '^11.0.0' + } + + return sort(dependencies) +} + +export function computeESLintConfig (devDependencies: { [key: string]: string }): any { + const keys = Object.keys(devDependencies) + // ts-standard + if (keys.includes('eslint-config-standard-with-typescript')) { + return { + extends: 'standard-with-typescript', + parserOptions: { + project: './tsconfig.json' + }, + overrides: [ + { + files: [ + 'test/**/*.test.ts' + ], + rules: { + '@typescript-eslint/no-floating-promises': 'off' + } + } + ] + } + } + // standard + if (keys.includes('eslint-config-standard')) { + return { + extends: 'standard' + } + } + // eslint + const config: { + extends: string[] + plugins: string[] + [key: string]: any + } = { + extends: ['eslint:recommended'], + plugins: [] + } + if (keys.includes('eslint-plugin-promise')) { + config.plugins.push('promise') + } + if (keys.includes('eslint-plugin-import')) { + config.extends.push('plugin:import/recommended') + config.plugins.push('import') + } + if (keys.includes('eslint-plugin-n')) { + config.extends.push('plugin:n/recommended') + } + if (keys.includes('eslint-plugin-node')) { + config.extends.push('plugin:node/recommended') + } + if (keys.includes('@typescript-eslint/eslint-plugin')) { + config.plugins.push('@typescript-eslint') + config.extends.push('plugin:@typescript-eslint/recommended') + } + if (keys.includes('@typescript-eslint/parser')) { + config.parser = '@typescript-eslint/parser' + } + return config +} diff --git a/src/utils/require.ts b/src/utils/require.ts new file mode 100644 index 00000000..bc943a38 --- /dev/null +++ b/src/utils/require.ts @@ -0,0 +1,111 @@ +import type _FastifyModule from 'fastify' +import { FastifyServerOptions } from 'fastify' +import { existsSync } from 'fs' +import { extname, resolve } from 'path' +import resolveFrom from 'resolve-from' +import { pathToFileURL } from 'url' +import { _import } from './import' + +/** + * Resolve the nearest module from `process.cwd` + * @param {string} module package name + * @returns {object} module + */ +export function _require (module: string): T { + if (existsSync(module)) { + // we remove the extension if we require module + const fullPath = resolve(module) + const extension = extname(module) + const path = fullPath.split(extension)[0] + return require(path) + } else { + return require(module) + } +} + +export type FastifyModule = typeof _FastifyModule + +/** + * Resolve the nearest fastify module from `process.cwd` + * @param {string} entry entry file + * @returns {object} fastify module + */ +export function _requireFastify (entry: string): FastifyModule { + try { + const baseDir = resolve(process.cwd(), entry) + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require(resolveFrom.silent(baseDir, 'fastify') ?? 'fastify') + } catch { + throw Error('unable to load fastify') + } +} + +/** + * Resolve the entry file from `process.cwd` + * @param {string} entry entry file + * @returns {function} plugin module + */ +export async function _requireEntryFile (entry: string): Promise<{ plugin: any, options: FastifyServerOptions }> { + const path = resolve(process.cwd(), entry) + if (!existsSync(path)) { + throw Error(`${path} doesn't exist within ${process.cwd()}`) + } + + const defScriptType = await computeDefaultScriptType(path) + const scriptType = computeScriptType(path, defScriptType) + + let plugin + let options + if (scriptType === 'module') { + plugin = await _import(pathToFileURL(path).href) + } else { + plugin = require(path) + } + // we check if custom option provided + if (typeof plugin.options === 'object') options = plugin.options + + // we check if it use default export + if (typeof plugin.default === 'function') plugin = plugin.default + + // we check it again, if custom option provided through default export + if (typeof plugin.options === 'object') options = plugin.options + + if (isInvalidAsyncPlugin(plugin)) { + throw new Error('Async/Await plugin function should contain 2 arguments. Refer to documentation for more information.') + } + + return { plugin, options } +} + +/** + * Validate plugin arguments + * @param {function} plugin plugin function + * @returns {boolean} + */ +export function isInvalidAsyncPlugin (plugin: Function): boolean { + return plugin.length !== 2 && plugin.constructor.name === 'AsyncFunction' +} + +/** + * Compute the file script type + * @param {string} name file name + * @param {string} def default script type + * @returns {string} script type + */ +export function computeScriptType (name: string, def?: 'commonjs' | 'module'): 'commonjs' | 'module' { + const regexpModulePattern = /\.mjs$/i + const regexpCommonJSPattern = /\.cjs$/i + return (regexpModulePattern.test(name) ? 'module' : regexpCommonJSPattern.test(name) ? 'commonjs' : def) ?? 'commonjs' +} + +/** + * Compute the script type from the nearest `package.json` + * @param {string} cwd `process.cwd` + * @returns {string} script type + */ +export async function computeDefaultScriptType (cwd: string): Promise<'commonjs' | 'module' | undefined> { + const { pkgUp } = await _import('pkg-up') + const pkg = await pkgUp({ cwd }) + // eslint-disable-next-line @typescript-eslint/no-var-requires + if (typeof pkg === 'string') return require(pkg).type +} diff --git a/src/utils/start.ts b/src/utils/start.ts new file mode 100644 index 00000000..c19593f1 --- /dev/null +++ b/src/utils/start.ts @@ -0,0 +1,198 @@ +import { FastifyInstance } from 'fastify' +import { constants } from 'fs' +import { access } from 'fs/promises' +import { resolve } from 'path' +import { _import } from './import' +import { FastifyModule, _require, _requireEntryFile, _requireFastify } from './require' + +// @ts-expect-error +let Fastify: FastifyModule = null + +/** + * Cache fastify module to global + * It is useful when `watch` is enabled + * @param {string} entry entry file + */ +function cacheFastify (entry: string): void { + try { + Fastify = _requireFastify(entry) + } catch (error: any) { + stop(error) + } +} + +export interface StartOption { + prefix: string + entry: string + require: string[] + port: number + address: string + debug: boolean + debugPort: number + debugAddress: string + pretty: boolean + watch: boolean + watchIgnorePattern: string + watchVerbose: boolean +} + +/** + * Start fastify instance + * @param {object} options options + * @returns + */ +export async function start (_o?: Partial): Promise { + const options = await normalizeStartOptions(_o) + + // we require the files before any actions + try { + for (const file of options.require) { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (file) { + /** + * This check ensures we ignore `-r ""`, trailing `-r`, or + * other silly things the user might (inadvertently) be doing. + */ + _require(file) + } + } + } catch (error: any) { + stop(error) + } + + // we update fastify reference + cacheFastify(options.entry) + + let entryPlugin = null + const fastifyOptions: any = { logger: { level: 'fatal' } } + + try { + const plugin = await _requireEntryFile(options.entry) + entryPlugin = plugin.plugin + Object.assign(fastifyOptions, plugin.options) + } catch (error: any) { + stop(error) + } + + if (options.pretty) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const PinoPretty = require('pino-pretty') + fastifyOptions.logger.stream = PinoPretty({ + colorize: true, + sync: true + }) + } + + // debug + if (options.debug) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const inspector = require('inspector') + inspector.open(options.debugPort, options.debugAddress) + } + + const fastify = Fastify(fastifyOptions) + + await fastify.register(entryPlugin, { prefix: options.prefix }) + + await fastify.listen({ port: options.port, host: options.address }) + + return fastify as any +} + +/** + * Stop the process + * @param {object|string} message error or message + * @returns + */ +export function stop (message?: Error | string): void { + if (message instanceof Error) { + console.log(message) + return process.exit(1) + } else if (message !== undefined) { + console.log(`Warn: ${message}`) + return process.exit(1) + } + process.exit() +} + +// we check options here +export async function normalizeStartOptions (options?: Partial): Promise { + options ??= {} + options.prefix = normalizePrefix(options.prefix) + options.entry = await normalizeEntry(options.entry) + options.require = normalizeRequire(options.require) + options.port = normalizePort(options.port) + options.address = await normalizeAddress(options.address) + options.debug = normalizeDebug(options.debug) + options.debugPort = normalizeDebugPort(options.debugPort) + options.debugAddress = await normalizeDebugAddress(options.debugAddress) + options.pretty = normalizePretty(options.pretty) + options.watch = normalizeWatch(options.watch) + options.watchIgnorePattern = normalizeWatchIgnorePattern(options.watchIgnorePattern) + options.watchVerbose = normalizeWatchVerbose(options.watchVerbose) + return options as StartOption +} + +export function normalizePrefix (prefix?: string): string { + return prefix ?? '' +} + +export async function normalizeEntry (entry?: string): Promise { + try { + if (typeof entry !== 'string') throw Error(`entry file expected to be a string, but recieved "${typeof entry}"`) + await access(resolve(process.cwd(), entry), constants.F_OK | constants.R_OK) + return entry + } catch { + throw Error(`entry file "${entry as string}" is not exist in ${process.cwd()} or do not have have permission to read.`) + } +} + +export function normalizeRequire (requires?: string | string[]): string[] { + const array: string[] = [] + if (requires === undefined) return array + requires = typeof requires === 'string' ? [requires] : requires + + for (const p of requires) { + if (p.trim() !== '') array.push(p.trim()) + } + + return array +} + +export function normalizePort (port?: number): number { + return process.env.PORT as any ?? port ?? 3000 +} + +export async function normalizeAddress (address?: string): Promise { + const { default: isDocker } = await _import('is-docker') + return address ?? (isDocker() === true ? '0.0.0.0' : 'localhost') +} + +export function normalizeDebug (debug?: boolean): boolean { + return debug ?? false +} + +export function normalizeDebugPort (debugPort?: number): number { + return debugPort ?? 9320 +} + +export async function normalizeDebugAddress (debugAddress?: string): Promise { + const { default: isDocker } = await _import('is-docker') + return debugAddress ?? (isDocker() === true ? '0.0.0.0' : 'localhost') +} + +export function normalizePretty (pretty?: boolean): boolean { + return pretty ?? false +} + +export function normalizeWatch (watch?: boolean): boolean { + return watch ?? false +} + +export function normalizeWatchIgnorePattern (watchIgnorePattern?: string): string { + return watchIgnorePattern ?? 'node_modules .git' +} + +export function normalizeWatchVerbose (watchVerbose?: boolean): boolean { + return watchVerbose ?? false +} diff --git a/src/utils/watch/constants.ts b/src/utils/watch/constants.ts new file mode 100644 index 00000000..2699a98d --- /dev/null +++ b/src/utils/watch/constants.ts @@ -0,0 +1 @@ +export const TIMEOUT = 5000 diff --git a/src/utils/watch/events.ts b/src/utils/watch/events.ts new file mode 100644 index 00000000..0708d0aa --- /dev/null +++ b/src/utils/watch/events.ts @@ -0,0 +1,2 @@ +export const GRACEFUL_SHUTDOWN = 'GRACEFUL SHUTDOWN' +export const READY = 'READY' diff --git a/src/utils/watch/fastify.ts b/src/utils/watch/fastify.ts new file mode 100644 index 00000000..b21d5f3c --- /dev/null +++ b/src/utils/watch/fastify.ts @@ -0,0 +1,55 @@ +import C from 'ansi-colors' +import { FastifyInstance } from 'fastify' +import { start, stop } from '../start' +import { TIMEOUT } from './constants' +import { GRACEFUL_SHUTDOWN, READY } from './events' + +function exit (this: any): void { + if (this as boolean) { console.log(C.red('[fastify-cli] process forced end')) } + process.exit(1) +} + +// shared instance +let fastify: null | FastifyInstance = null + +process.on('message', function (event) { + console.log('message', event) + if (event === GRACEFUL_SHUTDOWN) { + const message = C.red('[fastify-cli] process forced end') + setTimeout(exit.bind({ message }), TIMEOUT).unref() + if (fastify !== null) { + fastify.close(function () { + process.exit(0) + }) + } else { + process.exit(0) + } + } +}) + +process.on('uncaughtException', function (err) { + console.log(C.red(err as never as string)) + const message = C.red('[fastify-cli] app crashed - waiting for file changes before starting...') + exit.bind({ message })() +}) + +async function main (): Promise { + const type = process.env.CHILD_EVENT + let options: any = {} + if (typeof process.env.START_OPTIONS === 'string') options = JSON.parse(process.env.START_OPTIONS) + fastify = await start(options) + + process.send?.({ type, err: null }) + + try { + await fastify.ready() + process.send?.({ type: READY }) + } catch (err: any) { + stop(err) + } +} + +main().catch(function (err) { + console.error(err) + process.exit(1) +}) diff --git a/src/utils/watch/options.ts b/src/utils/watch/options.ts new file mode 100644 index 00000000..2aa282cd --- /dev/null +++ b/src/utils/watch/options.ts @@ -0,0 +1,8 @@ +export function normalizeIgnores (ignore: string): RegExp { + const ignores = ignore.split(' ').map((item) => item.trim()).filter((item) => item.length) + const regexp = ignores.map((file) => { + if (/^\./.test(file)) { return `\\${file}` } + return file + }).join('|') + return new RegExp(`(${regexp})`) +} diff --git a/src/utils/watch/watch.ts b/src/utils/watch/watch.ts new file mode 100644 index 00000000..429bfb36 --- /dev/null +++ b/src/utils/watch/watch.ts @@ -0,0 +1,113 @@ +import C from 'ansi-colors' +import { ChildProcess, fork } from 'child_process' +import EventEmitter from 'events' +import { join, relative } from 'path' +import { normalizeStartOptions, StartOption } from '../start' +import { GRACEFUL_SHUTDOWN } from './events' +import { normalizeIgnores } from './options' +import { spawnWatcher } from './watcher' + +const forkPath = join(__dirname, './fastify.js') + +export async function watch (_o?: Partial): Promise { + // we should spawn a instance that hold node process non-exit + const interval = setInterval(() => {}, 1000000) + const options = await normalizeStartOptions(_o) + const ee: EventEmitter & { stop: Function } = new EventEmitter() as any + let stopAll = false + let childs: ChildProcess[] = [] + + const stop = (watcher?: any, err?: any): void => { + for (const child of childs) { + child.kill() + } + + childs = [] + + if (err !== undefined) { + console.log(C.red(err)) + } + + if (watcher !== undefined) { + stopAll = true + clearInterval(interval) + return watcher.close() + } + } + + let isReady = false + + const run = (event: string): ChildProcess => { + const env = Object.assign({}, process.env, { CHILD_EVENT: event, START_OPTIONS: JSON.stringify(options) }) + + const child = fork(forkPath, { + env, + cwd: process.cwd() + }) + + child.on('exit', function (code, signal) { + if (code !== 0) { + stop() + } + if (childs.length === 0 && !stopAll) { + childs.push(run('restart')) + } + return null + }) + + child.on('message', function (event: { type: string, err?: any }) { + const { type, err } = event + if (err !== undefined && err !== null) { + ee.emit('error', err) + return + } + + if (type === 'ready') { + if (isReady) { + return + } + + isReady = true + } + + ee.emit(type, err) + }) + + return child + } + + childs.push(run('start')) + + const watcher = spawnWatcher(process.cwd(), normalizeIgnores(options.watchIgnorePattern)) + watcher.on('ready', function () { + watcher.on('all', function (event, path) { + console.log('watcher', event, path) + if (options.watchVerbose) { + console.log(C.gray(`[fastify-cli] watch - '${event}' occurred on '${relative(process.cwd(), path)}'`)) + } + + try { + const child = childs.shift() + child?.send(GRACEFUL_SHUTDOWN) + } catch (err: any) { + if (childs.length !== 0) { + console.log(C.red(err)) + stop(watcher, err) + } + childs.push(run('restart')) + } + }) + }) + + ee.on('error', function (err) { + stop(watcher, err) + }) + + ee.on('close', function () { + stop(watcher) + }) + + ee.stop = stop.bind(null, watcher) + + return ee +} diff --git a/src/utils/watch/watcher.ts b/src/utils/watch/watcher.ts new file mode 100644 index 00000000..12f8cb22 --- /dev/null +++ b/src/utils/watch/watcher.ts @@ -0,0 +1,5 @@ +import chokidar from 'chokidar' + +export function spawnWatcher (directory: string, ignored: RegExp): chokidar.FSWatcher { + return chokidar.watch(directory, { ignored }) +} diff --git a/start.js b/start.js deleted file mode 100755 index d11ecab3..00000000 --- a/start.js +++ /dev/null @@ -1,179 +0,0 @@ -#! /usr/bin/env node - -'use strict' - -require('dotenv').config() -const assert = require('assert') -const split = require('split2') -const PinoColada = require('pino-colada') -const pump = require('pump') -const isDocker = require('is-docker') -const closeWithGrace = require('close-with-grace') -const listenAddressDocker = '0.0.0.0' -const watch = require('./lib/watch') -const parseArgs = require('./args') -const { - exit, - requireModule, - requireFastifyForModule, - requireServerPluginFromPath, - showHelpForCommand -} = require('./util') - -let Fastify = null - -function loadModules (opts) { - try { - const { module: fastifyModule } = requireFastifyForModule(opts._[0]) - - Fastify = fastifyModule - } catch (e) { - module.exports.stop(e) - } -} - -async function start (args) { - const opts = parseArgs(args) - if (opts.help) { - return showHelpForCommand('start') - } - - if (opts._.length !== 1) { - console.error('Missing the required file parameter\n') - return showHelpForCommand('start') - } - - // we start crashing on unhandledRejection - require('make-promises-safe') - - loadModules(opts) - - if (opts.watch) { - return watch(args, opts.ignoreWatch, opts.verboseWatch) - } - - return runFastify(args) -} - -function stop (message) { - exit(message) -} - -async function runFastify (args, additionalOptions) { - const opts = parseArgs(args) - if (opts.require) { - if (typeof opts.require === 'string') { - opts.require = [opts.require] - } - - try { - opts.require.forEach(module => { - if (module) { - /* This check ensures we ignore `-r ""`, trailing `-r`, or - * other silly things the user might (inadvertently) be doing. - */ - requireModule(module) - } - }) - } catch (e) { - module.exports.stop(e) - } - } - opts.port = opts.port || process.env.PORT || 3000 - - loadModules(opts) - - let file = null - - try { - file = await requireServerPluginFromPath(opts._[0]) - } catch (e) { - return module.exports.stop(e) - } - - let logger - if (opts.loggingModule) { - try { - logger = requireModule(opts.loggingModule) - } catch (e) { - module.exports.stop(e) - } - } - - const defaultLogger = { - level: opts.logLevel - } - const options = { - logger: logger || defaultLogger, - - pluginTimeout: opts.pluginTimeout - } - - if (opts.bodyLimit) { - options.bodyLimit = opts.bodyLimit - } - - if (opts.prettyLogs) { - const stream = split(PinoColada()) - options.logger.stream = stream - pump(stream, process.stdout, assert.ifError) - } - - if (opts.debug) { - if (process.version.match(/v[0-6]\..*/g)) { - stop('Fastify debug mode not compatible with Node.js version < 6') - } else { - require('inspector').open( - opts.debugPort, - opts.debugHost || isDocker() ? listenAddressDocker : undefined - ) - } - } - - const fastify = Fastify( - opts.options ? Object.assign(options, file.options) : options - ) - - if (opts.prefix) { - opts.pluginOptions.prefix = opts.prefix - } - - const appConfig = Object.assign({}, opts.pluginOptions, additionalOptions) - await fastify.register(file.default || file, appConfig) - - const closeListeners = closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { - if (err) { - fastify.log.error(err) - } - await fastify.close() - }) - - await fastify.addHook('onClose', (instance, done) => { - closeListeners.uninstall() - done() - }) - - if (additionalOptions && additionalOptions.ready) { - await fastify.ready() - } else if (opts.address) { - await fastify.listen({ port: opts.port, host: opts.address }) - } else if (opts.socket) { - await fastify.listen(opts.socket) - } else if (isDocker()) { - await fastify.listen({ port: opts.port, host: listenAddressDocker }) - } else { - await fastify.listen({ port: opts.port }) - } - - return fastify -} - -function cli (args) { - start(args) -} - -module.exports = { start, stop, runFastify, cli } - -if (require.main === module) { - cli(process.argv.slice(2)) -} diff --git a/templates/app-ts/.vscode/launch.json b/templates/app-ts/.vscode/launch.json deleted file mode 100644 index 8c3b38e5..00000000 --- a/templates/app-ts/.vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "Launch Fastify", - "request": "launch", - "runtimeArgs": [ - "run", - "dev:start" - ], - "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], - "console": "integratedTerminal", - "preLaunchTask": "npm: watch:ts" - } - ] -} diff --git a/templates/app-ts/.vscode/tasks.json b/templates/app-ts/.vscode/tasks.json deleted file mode 100644 index 6d0c2bd6..00000000 --- a/templates/app-ts/.vscode/tasks.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "label": "npm: watch:ts", - "detail": "tsc -w", - "group": { - "type": "build", - "isDefault": true - }, - "script": "watch:ts", - "isBackground": true, - "problemMatcher": "$tsc-watch", - "presentation": { - "reveal": "never", - "group": "watchers" - } - } - ] -} diff --git a/templates/app-ts/README.md b/templates/app-ts/README.md deleted file mode 100644 index be35d934..00000000 --- a/templates/app-ts/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Getting Started with [Fastify-CLI](https://www.npmjs.com/package/fastify-cli) -This project was bootstrapped with Fastify-CLI. - -## Available Scripts - -In the project directory, you can run: - -### `npm run dev` - -To start the app in dev mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -### `npm start` - -For production mode - -### `npm run test` - -Run the test cases. - -## Learn More - -To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). diff --git a/templates/app-ts/__gitignore b/templates/app-ts/__gitignore deleted file mode 100644 index f4cefe89..00000000 --- a/templates/app-ts/__gitignore +++ /dev/null @@ -1,65 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -# 0x -profile-* - -# mac files -.DS_Store - -# vim swap files -*.swp - -# webstorm -.idea - -# vscode -.vscode -*code-workspace - -# clinic -profile* -*clinic* -*flamegraph* - -# generated code -examples/typescript-server.js -test/types/index.js - -# compiled app -dist diff --git a/templates/app-ts/__taprc b/templates/app-ts/__taprc deleted file mode 100644 index d6fd5343..00000000 --- a/templates/app-ts/__taprc +++ /dev/null @@ -1,4 +0,0 @@ -test-env: [ - TS_NODE_FILES=true, - TS_NODE_PROJECT=./test/tsconfig.json -] diff --git a/templates/app-ts/test/tsconfig.json b/templates/app-ts/test/tsconfig.json deleted file mode 100644 index 384d1712..00000000 --- a/templates/app-ts/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "baseUrl": ".", - "noEmit": true - }, - "include": ["../src/**/*.ts", "**/*.ts"] -} diff --git a/templates/app-ts/tsconfig.json b/templates/app-ts/tsconfig.json deleted file mode 100644 index 50dd0995..00000000 --- a/templates/app-ts/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "fastify-tsconfig", - "compilerOptions": { - "outDir": "dist", - "sourceMap": true - }, - "include": ["src/**/*.ts"] -} diff --git a/templates/app/.vscode/launch.json b/templates/app/.vscode/launch.json deleted file mode 100644 index 3e8a295a..00000000 --- a/templates/app/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "Launch Fastify", - "request": "launch", - "runtimeArgs": [ - "run", - "dev" - ], - "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], - "console": "integratedTerminal" - } - ] -} diff --git a/templates/app/__gitignore b/templates/app/__gitignore deleted file mode 100644 index 52962c25..00000000 --- a/templates/app/__gitignore +++ /dev/null @@ -1,58 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -# 0x -profile-* - -# mac files -.DS_Store - -# vim swap files -*.swp - -# webstorm -.idea - -# vscode -.vscode -*code-workspace - -# clinic -profile* -*clinic* -*flamegraph* diff --git a/templates/eject-ts/server.ts b/templates/eject-ts/server.ts deleted file mode 100644 index ecf4e5c6..00000000 --- a/templates/eject-ts/server.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Read the .env file. -import * as dotenv from "dotenv"; -dotenv.config(); - -// Require the framework -import Fastify from "fastify"; - -// Require library to exit fastify process, gracefully (if possible) -import closeWithGrace from "close-with-grace"; - -// Instantiate Fastify with some config -const app = Fastify({ - logger: true, -}); - -// Register your application as a normal plugin. -app.register(import("./app")); - -// delay is the number of milliseconds for the graceful close to finish -const closeListeners = closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { - if (err) { - app.log.error(err) - } - await app.close() -} as closeWithGrace.CloseWithGraceAsyncCallback) - -app.addHook('onClose', async (instance, done) => { - closeListeners.uninstall() - done() -}) - -// Start listening. -app.listen({ port: parseInt(process.env.PORT) || 3000 }, (err: any) => { - if (err) { - app.log.error(err); - process.exit(1); - } -}); diff --git a/templates/eject/server.js b/templates/eject/server.js deleted file mode 100644 index f114dcc4..00000000 --- a/templates/eject/server.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict' - -// Read the .env file. -require('dotenv').config() - -// Require the framework -const Fastify = require('fastify') - -// Require library to exit fastify process, gracefully (if possible) -const closeWithGrace = require('close-with-grace') - -// Instantiate Fastify with some config -const app = Fastify({ - logger: true -}) - -// Register your application as a normal plugin. -const appService = require('./app.js') -app.register(appService) - -// delay is the number of milliseconds for the graceful close to finish -const closeListeners = closeWithGrace({ delay: 500 }, async function ({ signal, err, manual }) { - if (err) { - app.log.error(err) - } - await app.close() -}) - -app.addHook('onClose', async (instance, done) => { - closeListeners.uninstall() - done() -}) - -// Start listening. -app.listen({ port: process.env.PORT || 3000 }, (err) => { - if (err) { - app.log.error(err) - process.exit(1) - } -}) diff --git a/templates/plugin/.github/workflows/ci.yml b/templates/plugin/.github/workflows/ci.yml deleted file mode 100644 index c8d01a63..00000000 --- a/templates/plugin/.github/workflows/ci.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: CI workflow -on: [push, pull_request] -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - node-version: [10.x, 12.x, 14.x] - os: [ubuntu-latest, windows-latest] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install Dependencies - run: npm install --ignore-scripts - - name: Test - run: npm run test diff --git a/templates/plugin/.taprc b/templates/plugin/.taprc deleted file mode 100644 index 31e63a78..00000000 --- a/templates/plugin/.taprc +++ /dev/null @@ -1,3 +0,0 @@ -ts: false -jsx: false -coverage: false diff --git a/templates/plugin/README.md b/templates/plugin/README.md deleted file mode 100644 index 00907c97..00000000 --- a/templates/plugin/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# __MY_PLUGIN__ - -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) ![CI workflow](__MY_PLUGIN_URL__ -/workflows/CI%20workflow/badge.svg) - -Supports Fastify versions `4.x` - -## Install -``` -npm i __MY_PLUGIN__ -``` - -## Usage -Require `__MY_PLUGIN__` and register. -```js -const fastify = require('fastify')() - -fastify.register(require('__MY_PLUGIN__'), { - // put your options here -}) - -fastify.listen({ port: 3000 }) -``` - -## Acknowledgements - -## License - -Licensed under [MIT](./LICENSE).
diff --git a/templates/plugin/index.d.ts b/templates/plugin/index.d.ts deleted file mode 100644 index 0063dc62..00000000 --- a/templates/plugin/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FastifyPluginCallback } from 'fastify' - -declare module 'fastify' { - export interface FastifyInstance { - // This is an example decorator type added to fastify - exampleDecorator: () => string - } -} - -declare const example: FastifyPluginCallback<() => string> - -export { example } -export default example diff --git a/templates/plugin/index.js b/templates/plugin/index.js deleted file mode 100644 index 9fa66d95..00000000 --- a/templates/plugin/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const fp = require('fastify-plugin') - -module.exports = fp(async function (fastify, opts) { - fastify.decorate('exampleDecorator', () => { - return 'decorated' - }) -}, { fastify: '^4.x' }) diff --git a/templates/plugin/test/index.test-d.ts b/templates/plugin/test/index.test-d.ts deleted file mode 100644 index 4f28c85a..00000000 --- a/templates/plugin/test/index.test-d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import fastify from 'fastify' -import example from '..' -import { expectType } from 'tsd' - -let app -try { - app = fastify() - await app.ready() - app.register(example) - expectType<() => string>(app.exampleDecorator) -} catch (err) { - console.error(err) -} diff --git a/templates/plugin/test/index.test.js b/templates/plugin/test/index.test.js deleted file mode 100644 index e914f8f4..00000000 --- a/templates/plugin/test/index.test.js +++ /dev/null @@ -1,13 +0,0 @@ -const { test } = require('tap') - -test('should register the correct decorator', async t => { - t.plan(1) - - const app = require('fastify')() - - app.register(require('..')) - - await app.ready() - - t.same(app.exampleDecorator(), 'decorated') -}) diff --git a/templates/plugin/tsconfig.json b/templates/plugin/tsconfig.json deleted file mode 100644 index fb5c357a..00000000 --- a/templates/plugin/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "fastify-tsconfig", - "compilerOptions": { - "noEmit": true - }, - "include": [ - "**/*.ts" - ] -} diff --git a/templates/app/README.md b/templates/project/README.md similarity index 94% rename from templates/app/README.md rename to templates/project/README.md index d3497b90..5a7a8277 100644 --- a/templates/app/README.md +++ b/templates/project/README.md @@ -20,4 +20,4 @@ Run the test cases. ## Learn More -To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). +To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). \ No newline at end of file diff --git a/templates/plugin/.gitignore b/templates/project/__gitignore.ejs similarity index 53% rename from templates/plugin/.gitignore rename to templates/project/__gitignore.ejs index 6214c438..d071b972 100644 --- a/templates/plugin/.gitignore +++ b/templates/project/__gitignore.ejs @@ -4,6 +4,10 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids @@ -16,6 +20,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage +*.lcov # nyc test coverage .nyc_output @@ -36,8 +41,11 @@ build/Release node_modules/ jspm_packages/ -# TypeScript v1 declaration files -typings/ +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo # Optional npm cache directory .npm @@ -45,6 +53,12 @@ typings/ # Optional eslint cache .eslintcache +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + # Optional REPL history .node_repl_history @@ -60,12 +74,21 @@ typings/ # parcel-bundler cache (https://parceljs.org/) .cache +.parcel-cache -# next.js build output +# Next.js build output .next +out -# nuxt.js build output +# Nuxt.js build / generate output .nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public # vuepress build output .vuepress/dist @@ -78,3 +101,34 @@ typings/ # DynamoDB Local files .dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# lock files +package-lock.json +yarn.lock + +# editor files +.vscode +.idea + +<% if(language === "TypeScript") { %> +**/*.js +<% } %> \ No newline at end of file diff --git a/templates/app/app.js b/templates/project/app.js similarity index 100% rename from templates/app/app.js rename to templates/project/app.js diff --git a/templates/app-ts/src/plugins/README.md b/templates/project/plugins/README.md similarity index 100% rename from templates/app-ts/src/plugins/README.md rename to templates/project/plugins/README.md diff --git a/templates/app/plugins/sensible.js b/templates/project/plugins/sensible.js similarity index 100% rename from templates/app/plugins/sensible.js rename to templates/project/plugins/sensible.js diff --git a/templates/app/plugins/support.js b/templates/project/plugins/support.js similarity index 100% rename from templates/app/plugins/support.js rename to templates/project/plugins/support.js diff --git a/templates/app/routes/README.md b/templates/project/routes/README.md similarity index 96% rename from templates/app/routes/README.md rename to templates/project/routes/README.md index 4cd0dcba..b565884a 100644 --- a/templates/app/routes/README.md +++ b/templates/project/routes/README.md @@ -11,7 +11,7 @@ plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is encapsulated (it can have its own independent plugins) and it is typically stored in a file; be careful to group your routes logically, e.g. all `/users` routes in a `users.js` file. We have added -a `root.js` file for you with a '/' root added. +a `root.js` file for you with a '/' root added. If a single file become too large, create a folder and add a `index.js` file there: this file must be a Fastify plugin, and it will be loaded automatically diff --git a/templates/app/routes/example/index.js b/templates/project/routes/example/index.js similarity index 100% rename from templates/app/routes/example/index.js rename to templates/project/routes/example/index.js diff --git a/templates/app/routes/root.js b/templates/project/routes/root.js similarity index 100% rename from templates/app/routes/root.js rename to templates/project/routes/root.js diff --git a/templates/app-ts/src/app.ts b/templates/project/src/app.ts similarity index 74% rename from templates/app-ts/src/app.ts rename to templates/project/src/app.ts index e1f077b2..5d72e581 100644 --- a/templates/app-ts/src/app.ts +++ b/templates/project/src/app.ts @@ -1,14 +1,14 @@ -import { join } from 'path'; -import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload'; -import { FastifyPluginAsync } from 'fastify'; +import AutoLoad, { AutoloadPluginOptions } from '@fastify/autoload' +import { FastifyPluginAsync } from 'fastify' +import { join } from 'path' export type AppOptions = { // Place your custom options for app below here. -} & Partial; +} & Partial const app: FastifyPluginAsync = async ( - fastify, - opts + fastify, + opts ): Promise => { // Place here your custom code! @@ -28,8 +28,7 @@ const app: FastifyPluginAsync = async ( dir: join(__dirname, 'routes'), options: opts }) +} -}; - -export default app; +export default app export { app } diff --git a/templates/app/plugins/README.md b/templates/project/src/plugins/README.md similarity index 100% rename from templates/app/plugins/README.md rename to templates/project/src/plugins/README.md diff --git a/templates/app-ts/src/plugins/sensible.ts b/templates/project/src/plugins/sensible.ts similarity index 70% rename from templates/app-ts/src/plugins/sensible.ts rename to templates/project/src/plugins/sensible.ts index fb338165..a6edf318 100644 --- a/templates/app-ts/src/plugins/sensible.ts +++ b/templates/project/src/plugins/sensible.ts @@ -1,13 +1,13 @@ -import fp from 'fastify-plugin' import sensible, { SensibleOptions } from '@fastify/sensible' +import fp from 'fastify-plugin' /** * This plugins adds some utilities to handle http errors * * @see https://github.com/fastify/fastify-sensible */ -export default fp(async (fastify, opts) => { - fastify.register(sensible, { +export default fp(async function (fastify, opts) { + await fastify.register(sensible, { errorHandler: false }) }) diff --git a/templates/app-ts/src/plugins/support.ts b/templates/project/src/plugins/support.ts similarity index 72% rename from templates/app-ts/src/plugins/support.ts rename to templates/project/src/plugins/support.ts index 94bae4f5..205d30a6 100644 --- a/templates/app-ts/src/plugins/support.ts +++ b/templates/project/src/plugins/support.ts @@ -1,12 +1,13 @@ import fp from 'fastify-plugin' +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SupportPluginOptions { // Specify Support plugin options here } // The use of fastify-plugin is required to be able // to export the decorators to the outer scope -export default fp(async (fastify, opts) => { +export default fp(async function (fastify, opts) { fastify.decorate('someSupport', function () { return 'hugs' }) @@ -15,6 +16,6 @@ export default fp(async (fastify, opts) => { // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { export interface FastifyInstance { - someSupport(): string; + someSupport: () => string } } diff --git a/templates/app-ts/src/routes/README.md b/templates/project/src/routes/README.md similarity index 76% rename from templates/app-ts/src/routes/README.md rename to templates/project/src/routes/README.md index 67b739aa..b565884a 100644 --- a/templates/app-ts/src/routes/README.md +++ b/templates/project/src/routes/README.md @@ -1,6 +1,6 @@ # Routes Folder -Routes define endpoints within your application. Fastify provides an +Routes define routes within your application. Fastify provides an easy path to a microservice architecture, in the future you might want to independently deploy some of those. @@ -11,7 +11,7 @@ plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is encapsulated (it can have its own independent plugins) and it is typically stored in a file; be careful to group your routes logically, e.g. all `/users` routes in a `users.js` file. We have added -a `root.js` file for you with a '/' root added. +a `root.js` file for you with a '/' root added. If a single file become too large, create a folder and add a `index.js` file there: this file must be a Fastify plugin, and it will be loaded automatically @@ -22,3 +22,6 @@ and eventually extract them. If you need to share functionality between routes, place that functionality into the `plugins` folder, and share it via [decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). + +If you're a bit confused about using `async/await` to write routes, you would +better take a look at [Promise resolution](https://www.fastify.io/docs/latest/Routes/#promise-resolution) for more details. diff --git a/templates/app-ts/src/routes/example/index.ts b/templates/project/src/routes/example/index.ts similarity index 71% rename from templates/app-ts/src/routes/example/index.ts rename to templates/project/src/routes/example/index.ts index 819c5e77..68a0ea13 100644 --- a/templates/app-ts/src/routes/example/index.ts +++ b/templates/project/src/routes/example/index.ts @@ -1,4 +1,4 @@ -import { FastifyPluginAsync } from "fastify" +import { FastifyPluginAsync } from 'fastify' const example: FastifyPluginAsync = async (fastify, opts): Promise => { fastify.get('/', async function (request, reply) { @@ -6,4 +6,4 @@ const example: FastifyPluginAsync = async (fastify, opts): Promise => { }) } -export default example; +export default example diff --git a/templates/app-ts/src/routes/root.ts b/templates/project/src/routes/root.ts similarity index 90% rename from templates/app-ts/src/routes/root.ts rename to templates/project/src/routes/root.ts index 2a1b3342..27918c71 100644 --- a/templates/app-ts/src/routes/root.ts +++ b/templates/project/src/routes/root.ts @@ -6,4 +6,4 @@ const root: FastifyPluginAsync = async (fastify, opts): Promise => { }) } -export default root; +export default root diff --git a/templates/app/test/helper.js b/templates/project/test/helper.js similarity index 100% rename from templates/app/test/helper.js rename to templates/project/test/helper.js diff --git a/templates/app-ts/test/helper.ts b/templates/project/test/helper.ts similarity index 60% rename from templates/app-ts/test/helper.ts rename to templates/project/test/helper.ts index 72aad8d6..604e971c 100644 --- a/templates/app-ts/test/helper.ts +++ b/templates/project/test/helper.ts @@ -1,19 +1,19 @@ // This file contains code that we reuse between our tests. -import Fastify from 'fastify' +import Fastify, { FastifyInstance } from 'fastify' import fp from 'fastify-plugin' +import * as tap from 'tap' import App from '../src/app' -import * as tap from 'tap'; -export type Test = typeof tap['Test']['prototype']; +export type Test = typeof tap['Test']['prototype'] // Fill in this config with all the configurations // needed for testing the application -async function config () { +async function config (): Promise { return {} } // Automatically build and tear down our instance -async function build (t: Test) { +async function build (t: Test): Promise { const app = Fastify() // fastify-plugin ensures that all decorators @@ -21,12 +21,13 @@ async function build (t: Test) { // different from the production setup void app.register(fp(App), await config()) - await app.ready(); + await app.ready() // Tear down our app after we are done - t.teardown(() => void app.close()) + // eslint-disable-next-line @typescript-eslint/promise-function-async + t.teardown(() => app.close()) - return app + return await app } export { diff --git a/templates/app/test/plugins/support.test.js b/templates/project/test/plugins/support.test.js similarity index 100% rename from templates/app/test/plugins/support.test.js rename to templates/project/test/plugins/support.test.js diff --git a/templates/app-ts/test/plugins/support.test.ts b/templates/project/test/plugins/support.test.ts similarity index 100% rename from templates/app-ts/test/plugins/support.test.ts rename to templates/project/test/plugins/support.test.ts index d67c06a1..2376ee6b 100644 --- a/templates/app-ts/test/plugins/support.test.ts +++ b/templates/project/test/plugins/support.test.ts @@ -1,5 +1,5 @@ -import { test } from 'tap' import Fastify from 'fastify' +import { test } from 'tap' import Support from '../../src/plugins/support' test('support works standalone', async (t) => { diff --git a/templates/app/test/routes/example.test.js b/templates/project/test/routes/example.test.js similarity index 100% rename from templates/app/test/routes/example.test.js rename to templates/project/test/routes/example.test.js diff --git a/templates/app-ts/test/routes/example.test.ts b/templates/project/test/routes/example.test.ts similarity index 100% rename from templates/app-ts/test/routes/example.test.ts rename to templates/project/test/routes/example.test.ts diff --git a/templates/app/test/routes/root.test.js b/templates/project/test/routes/root.test.js similarity index 100% rename from templates/app/test/routes/root.test.js rename to templates/project/test/routes/root.test.js diff --git a/templates/app-ts/test/routes/root.test.ts b/templates/project/test/routes/root.test.ts similarity index 100% rename from templates/app-ts/test/routes/root.test.ts rename to templates/project/test/routes/root.test.ts diff --git a/templates/project/tsconfig.build.json b/templates/project/tsconfig.build.json new file mode 100644 index 00000000..08c344e7 --- /dev/null +++ b/templates/project/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "moduleResolution": "Node", + "resolveJsonModule": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "skipLibCheck": true, + "esModuleInterop": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/templates/project/tsconfig.json b/templates/project/tsconfig.json new file mode 100644 index 00000000..f8f12e34 --- /dev/null +++ b/templates/project/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "moduleResolution": "Node", + "resolveJsonModule": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "skipLibCheck": true, + "esModuleInterop": true, + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/templates/readme/README.md b/templates/readme/README.md deleted file mode 100644 index 87d1b80c..00000000 --- a/templates/readme/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# __packageName__ - -Short description of the purpose of your plugin. - -## Install - -``` -npm install __packageName__ --save -yarn add __packageName__ -``` - -## Example - -```js -const fastify = require('fastify')() -fastify.register(require('__packageName__')) -fastify.listen({ port: 3000 }) -``` - -You can also start any Fastify plugin with the [Fastify-cli](https://github.com/fastify/fastify-cli): - -``` -fastify start __pluginFileName__ -``` - -## Plugin - -### Accessibility - - -__accessibilityTemplate__ - -### Decorators - - -#### Fastify - - -__fastifyDecorators__ - -#### Reply - - -__replyDecorators__ - -## Dependencies - -__pluginDeps__ - -## Compatible Fastify version - -__minFastify__ \ No newline at end of file diff --git a/test/args.test.js b/test/args.test.js deleted file mode 100644 index e72a197b..00000000 --- a/test/args.test.js +++ /dev/null @@ -1,255 +0,0 @@ -const t = require('tap') -const test = t.test -const parseArgs = require('../args') - -test('should parse args correctly', t => { - t.plan(1) - - const argv = [ - '--port', '7777', - '--address', 'fastify.io:9999', - '--socket', 'fastify.io.socket:9999', - '--require', './require-module.js', - '--log-level', 'info', - '--pretty-logs', 'true', - '--watch', 'true', - '--ignore-watch', 'ignoreme.js', - '--verbose-watch', 'true', - '--options', 'true', - '--prefix', 'FASTIFY_', - '--plugin-timeout', '500', - '--body-limit', '5242880', - '--debug', 'true', - '--debug-port', 1111, - '--debug-host', '1.1.1.1', - '--logging-module', './custom-logger.js', - 'app.js' - ] - const parsedArgs = parseArgs(argv) - - t.strictSame(parsedArgs, { - _: ['app.js'], - '--': [], - prettyLogs: true, - options: true, - watch: true, - ignoreWatch: 'node_modules build dist .git bower_components logs .swp .nyc_output ignoreme.js', - verboseWatch: true, - port: 7777, - address: 'fastify.io:9999', - socket: 'fastify.io.socket:9999', - require: './require-module.js', - logLevel: 'info', - prefix: 'FASTIFY_', - pluginTimeout: 500, - pluginOptions: {}, - bodyLimit: 5242880, - debug: true, - debugPort: 1111, - debugHost: '1.1.1.1', - loggingModule: './custom-logger.js', - lang: 'js' - }) -}) - -test('should parse args with = assignment correctly', t => { - t.plan(1) - - const argv = [ - '--port=7777', - '--address=fastify.io:9999', - '--socket=fastify.io.socket:9999', - '--require', './require-module.js', - '--log-level=info', - '--pretty-logs=true', - '--watch=true', - '--ignore-watch=ignoreme.js', - '--verbose-watch=true', - '--options=true', - '--prefix=FASTIFY_', - '--plugin-timeout=500', - '--body-limit=5242880', - '--debug=true', - '--debug-port', 1111, - '--debug-host', '1.1.1.1', - '--logging-module', './custom-logger.js', - 'app.js' - ] - const parsedArgs = parseArgs(argv) - - t.strictSame(parsedArgs, { - _: ['app.js'], - '--': [], - prettyLogs: true, - options: true, - watch: true, - ignoreWatch: 'node_modules build dist .git bower_components logs .swp .nyc_output ignoreme.js', - verboseWatch: true, - port: 7777, - address: 'fastify.io:9999', - socket: 'fastify.io.socket:9999', - require: './require-module.js', - logLevel: 'info', - prefix: 'FASTIFY_', - pluginTimeout: 500, - pluginOptions: {}, - bodyLimit: 5242880, - debug: true, - debugPort: 1111, - debugHost: '1.1.1.1', - loggingModule: './custom-logger.js', - lang: 'js' - }) -}) - -test('should parse env vars correctly', t => { - t.plan(1) - - process.env.FASTIFY_PORT = '7777' - process.env.FASTIFY_ADDRESS = 'fastify.io:9999' - process.env.FASTIFY_SOCKET = 'fastify.io.socket:9999' - process.env.FASTIFY_REQUIRE = './require-module.js' - process.env.FASTIFY_LOG_LEVEL = 'info' - process.env.FASTIFY_PRETTY_LOGS = 'true' - process.env.FASTIFY_WATCH = 'true' - process.env.FASTIFY_IGNORE_WATCH = 'ignoreme.js' - process.env.FASTIFY_VERBOSE_WATCH = 'true' - process.env.FASTIFY_OPTIONS = 'true' - process.env.FASTIFY_PREFIX = 'FASTIFY_' - process.env.FASTIFY_BODY_LIMIT = '5242880' - process.env.FASTIFY_PLUGIN_TIMEOUT = '500' - process.env.FASTIFY_DEBUG = 'true' - process.env.FASTIFY_DEBUG_PORT = '1111' - process.env.FASTIFY_DEBUG_HOST = '1.1.1.1' - process.env.FASTIFY_LOGGING_MODULE = './custom-logger.js' - - t.teardown(function () { - delete process.env.FASTIFY_PORT - delete process.env.FASTIFY_ADDRESS - delete process.env.FASTIFY_SOCKET - delete process.env.FASTIFY_REQUIRE - delete process.env.FASTIFY_LOG_LEVEL - delete process.env.FASTIFY_PRETTY_LOGS - delete process.env.FASTIFY_WATCH - delete process.env.FASTIFY_IGNORE_WATCH - delete process.env.FASTIFY_VERBOSE_WATCH - delete process.env.FASTIFY_OPTIONS - delete process.env.FASTIFY_PREFIX - delete process.env.FASTIFY_BODY_LIMIT - delete process.env.FASTIFY_PLUGIN_TIMEOUT - delete process.env.FASTIFY_DEBUG - delete process.env.FASTIFY_DEBUG_PORT - delete process.env.FASTIFY_LOGGING_MODULE - }) - - const parsedArgs = parseArgs([]) - - t.strictSame(parsedArgs, { - _: [], - '--': [], - prettyLogs: true, - options: true, - watch: true, - ignoreWatch: 'node_modules build dist .git bower_components logs .swp .nyc_output ignoreme.js', - verboseWatch: true, - address: 'fastify.io:9999', - bodyLimit: 5242880, - logLevel: 'info', - port: 7777, - prefix: 'FASTIFY_', - socket: 'fastify.io.socket:9999', - require: './require-module.js', - pluginTimeout: 500, - pluginOptions: {}, - debug: true, - debugPort: 1111, - debugHost: '1.1.1.1', - loggingModule: './custom-logger.js', - lang: 'js' - }) -}) - -test('should respect default values', t => { - t.plan(12) - - const argv = [ - 'app.js' - ] - - const parsedArgs = parseArgs(argv) - - t.equal(parsedArgs._[0], 'app.js') - t.equal(parsedArgs.options, false) - t.equal(parsedArgs.prettyLogs, false) - t.equal(parsedArgs.watch, false) - t.equal(parsedArgs.ignoreWatch, 'node_modules build dist .git bower_components logs .swp .nyc_output') - t.equal(parsedArgs.verboseWatch, false) - t.equal(parsedArgs.logLevel, 'fatal') - t.equal(parsedArgs.pluginTimeout, 10000) - t.equal(parsedArgs.debug, false) - t.equal(parsedArgs.debugPort, 9320) - t.equal(parsedArgs.loggingModule, undefined) - t.equal(parsedArgs.require, undefined) -}) - -test('should parse custom plugin options', t => { - t.plan(1) - - const argv = [ - '--port', '7777', - '--address', 'fastify.io:9999', - '--socket', 'fastify.io.socket:9999', - '--require', './require-module.js', - '--log-level', 'info', - '--pretty-logs', 'true', - '--watch', 'true', - '--ignore-watch', 'ignoreme.js', - '--verbose-watch', 'true', - '--options', 'true', - '--prefix', 'FASTIFY_', - '--plugin-timeout', '500', - '--body-limit', '5242880', - '--debug', 'true', - '--debug-port', 1111, - '--debug-host', '1.1.1.1', - '--logging-module', './custom-logger.js', - 'app.js', - '--', - '-abc', - '--hello', 'world' - ] - const parsedArgs = parseArgs(argv) - - t.strictSame(parsedArgs, { - _: ['app.js'], - '--': [ - '-abc', - '--hello', - 'world' - ], - prettyLogs: true, - options: true, - watch: true, - ignoreWatch: 'node_modules build dist .git bower_components logs .swp .nyc_output ignoreme.js', - verboseWatch: true, - port: 7777, - address: 'fastify.io:9999', - socket: 'fastify.io.socket:9999', - require: './require-module.js', - logLevel: 'info', - prefix: 'FASTIFY_', - pluginTimeout: 500, - pluginOptions: { - a: true, - b: true, - c: true, - hello: 'world' - }, - bodyLimit: 5242880, - debug: true, - debugPort: 1111, - debugHost: '1.1.1.1', - loggingModule: './custom-logger.js', - lang: 'js' - }) -}) diff --git a/test/cli.test.js b/test/cli.test.js deleted file mode 100644 index bc4ce36d..00000000 --- a/test/cli.test.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const t = require('tap') -const { execSync } = require('child_process') -const { mkdirSync } = require('fs') -const path = require('path') -const rimraf = require('rimraf') - -const workdir = path.join(__dirname, 'workdir') -const target = path.join(workdir, 'cli.test') - -t.plan(1) - -rimraf.sync(workdir) -mkdirSync(workdir, { recursive: true }) - -execSync(`node cli.js generate ${target}`) - -t.pass() diff --git a/test/commands/generate/project.test.ts b/test/commands/generate/project.test.ts new file mode 100644 index 00000000..d7c01e56 --- /dev/null +++ b/test/commands/generate/project.test.ts @@ -0,0 +1,174 @@ +import { mkdir, rm } from 'fs/promises' +import { join, resolve } from 'path' +import t from 'tap' +import { INGORE_GENERATE_TEST, INGORE_NPM_INSTALL } from '../../env' +import { ENTER, KEY_DOWN, runCommand, runRawCommand } from '../../run-command' + +t.test('terminate on folder exist and not overwrite', async (t) => { + const name = 'terminate-not-overwrite' + const location = resolve(join('test', 'fixtures', name)) + + await mkdir(location, { recursive: true }) + + t.teardown(async () => { + await rm(location, { recursive: true, force: true }) + }) + + t.plan(4) + + const { stdout, stdin } = runCommand(['generate', 'project']) + + await stdout.until(/What is your project name?/) + t.pass('ask project name') + stdin.writeLn(name) + await stdout.until(/Where do you want to place your project?/) + t.pass('ask project location') + stdin.writeLn(location) + await stdout.until(/The folder already exist. Do you want to overwrite?/) + t.pass('ask project overwrite') + stdin.writeLn('N') + await stdout.until(/Terminated because location cannot be overwrite./) + t.pass('command terminated') +}) + +t.test('re-input on invalid name', async (t) => { + const name = 'terminate-not-overwrite' + const location = resolve(join('test', 'fixtures', name)) + + await mkdir(location, { recursive: true }) + + t.teardown(async () => { + await rm(location, { recursive: true, force: true }) + }) + + t.plan(6) + + const { stdout, stdin } = runCommand(['generate', 'project']) + + await stdout.until(/What is your project name?/) + t.pass('ask project name') + stdin.writeLn('') + await stdout.until(/Project Name cannot be empty./) + t.pass('ask when invalid name - empty string') + stdin.press(ENTER) + await stdout.until(/Project Name cannot be empty./) + t.pass('ask when invalid name - enter') + stdin.writeLn(name) + await stdout.until(/Where do you want to place your project?/) + t.pass('ask project location') + stdin.writeLn(location) + await stdout.until(/The folder already exist. Do you want to overwrite?/) + t.pass('ask project overwrite') + stdin.writeLn('N') + await stdout.until(/Terminated because location cannot be overwrite./) + t.pass('command terminated') +}) + +t.test('JavaScript + Standard', async (t) => { + const name = 'javascript-standard' + const location = resolve(join('test', 'fixtures', name)) + + t.teardown(async () => { + await rm(location, { recursive: true, force: true }) + }) + + t.plan(INGORE_NPM_INSTALL ? 8 : 9) + const { stdout, stdin } = runCommand(['generate', 'project']) + + await stdout.until(/What is your project name?/) + t.pass('ask project name') + stdin.writeLn(name) + await stdout.until(/Where do you want to place your project?/) + t.pass('ask project location') + stdin.writeLn(location) + await stdout.until(/Which language will you use?/) + t.pass('ask project language') + stdin.press(ENTER) + await stdout.until(/Which linter would you like to use?/) + t.pass('ask project lint') + stdin.press(ENTER) + await stdout.until(/Which test framework would you like to use?/) + t.pass('ask project test framework') + stdin.press(ENTER) + await stdout.until(/Do you want to run "npm install"?/) + t.pass('ask project npm install') + if (INGORE_NPM_INSTALL) { + stdin.writeLn('N') + } else { + stdin.press(ENTER) + await stdout.until(/initialized in/) + t.pass('project node_modules installed') + } + + t.test('Lint', { skip: INGORE_NPM_INSTALL }, async (t) => { + t.plan(2) + const { stdout, stderr, exited } = runRawCommand(['npm', 'run', 'lint'], { cwd: location }) + await exited + t.same(stderr.lines, [], 'no stderr') + t.matchSnapshot(stdout.lines) + }) + + t.test('Test', { skip: INGORE_GENERATE_TEST }, async (t) => { + t.plan(2) + const { stdout, stderr, exited } = runRawCommand(['npm', 'run', 'test'], { cwd: location }) + await exited + t.same(stderr.lines, [], 'no stderr') + t.matchSnapshot(stdout.lines) + }) +}) + +t.test('TypeScript + ESLint + TSStandard', async (t) => { + const name = 'typescript-eslint-ts-standard' + const location = resolve(join('test', 'fixtures', name)) + + t.teardown(async () => { + await rm(location, { recursive: true, force: true }) + }) + + t.plan(INGORE_NPM_INSTALL ? 8 : 9) + const { stdout, stdin } = runCommand(['generate', 'project']) + + await stdout.until(/What is your project name?/) + t.pass('ask project name') + stdin.writeLn(name) + await stdout.until(/Where do you want to place your project?/) + t.pass('ask project location') + stdin.writeLn(location) + await stdout.until(/Which language will you use?/) + t.pass('ask project language') + stdin.press(KEY_DOWN) + stdin.press(ENTER) + await stdout.until(/Which linter would you like to use?/) + t.pass('ask project lint') + stdin.press(KEY_DOWN) + stdin.press(KEY_DOWN) + stdin.press(ENTER) + await stdout.until(/Which test framework would you like to use?/) + t.pass('ask project test framework') + stdin.press(ENTER) + await stdout.until(/Do you want to run "npm install"?/) + t.pass('ask project npm install') + if (INGORE_NPM_INSTALL) { + stdin.writeLn('N') + } else { + stdin.press(ENTER) + await stdout.until(/initialized in/) + t.pass('project node_modules installed') + } + + t.test('Lint', { skip: INGORE_NPM_INSTALL }, async (t) => { + t.plan(2) + const { stdout, stderr, exited } = runRawCommand(['npm', 'run', 'lint'], { cwd: location }) + await exited + t.same(stderr.lines, [], 'no stderr') + t.matchSnapshot(stdout.lines) + }) + + t.test('Test', { skip: INGORE_GENERATE_TEST }, async (t) => { + t.plan(2) + const { stdout, stderr, exited } = runRawCommand(['npm', 'run', 'test'], { cwd: location }) + await exited + t.same(stderr.lines, [], 'no stderr') + t.matchSnapshot(stdout.lines) + }) +}) diff --git a/test/data/async-plugin-with-one-argument.js b/test/data/async-plugin-with-one-argument.js deleted file mode 100644 index 57a95ce1..00000000 --- a/test/data/async-plugin-with-one-argument.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict' - -// This is a counter example. WILL NOT WORK! -// we are creating an async plugin with only one argument -module.exports = async function (fastify) { -} diff --git a/test/data/custom-logger.js b/test/data/custom-logger.js deleted file mode 100644 index f58b07aa..00000000 --- a/test/data/custom-logger.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: 'Custom Logger', - customLevels: { - test: 99 - } -} diff --git a/test/data/custom-require.js b/test/data/custom-require.js deleted file mode 100644 index 3fa1a938..00000000 --- a/test/data/custom-require.js +++ /dev/null @@ -1,3 +0,0 @@ -/* global GLOBAL_MODULE_1 */ -GLOBAL_MODULE_1 = true // eslint-disable-line -console.log('this is module to be preloaded that sets GLOBAL_MODULE_1=', GLOBAL_MODULE_1) diff --git a/test/data/custom-require2.js b/test/data/custom-require2.js deleted file mode 100644 index e92e5f6f..00000000 --- a/test/data/custom-require2.js +++ /dev/null @@ -1,3 +0,0 @@ -/* global GLOBAL_MODULE_2 */ -GLOBAL_MODULE_2 = true // eslint-disable-line -console.log('this is module to be preloaded that sets GLOBAL_MODULE_2=', GLOBAL_MODULE_2) diff --git a/test/data/package-not-found.js b/test/data/package-not-found.js deleted file mode 100644 index 6563c58b..00000000 --- a/test/data/package-not-found.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict' - -require('unknown-package') diff --git a/test/data/parsing-error.js b/test/data/parsing-error.js deleted file mode 100644 index eef843b5..00000000 --- a/test/data/parsing-error.js +++ /dev/null @@ -1 +0,0 @@ -' diff --git a/test/data/rejection.js b/test/data/rejection.js deleted file mode 100644 index 8c6e4a24..00000000 --- a/test/data/rejection.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -// This is a counter example. WILL NOT WORK! -// we are creating an unhandledRejection on purpose -module.exports = function (fastify, opts, next) { - Promise.reject(new Error('there is no catch')) -} diff --git a/test/data/timeout-plugin.js b/test/data/timeout-plugin.js deleted file mode 100644 index fd15a61a..00000000 --- a/test/data/timeout-plugin.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -// This is a counter example. WILL NOT WORK! -// the next function is not called -module.exports = function (fastify, opts, next) { - fastify.get('/', function (req, reply) { - reply.send({ wont: 'work' }) - }) - // next() not called on purpose -} diff --git a/test/data/undefinedVariable.js b/test/data/undefinedVariable.js deleted file mode 100644 index 1e2e4575..00000000 --- a/test/data/undefinedVariable.js +++ /dev/null @@ -1 +0,0 @@ -undefinedVariable.pippo.pluto.paperino = 5 diff --git a/test/eject-ts.test.js b/test/eject-ts.test.js deleted file mode 100644 index a944d820..00000000 --- a/test/eject-ts.test.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict' - -// bailout if a test is broken -// so that the folder can be inspected -process.env.TAP_BAIL = true - -const t = require('tap') -const { mkdirSync, readFileSync, readFile } = require('fs') -const path = require('path') -const rimraf = require('rimraf') -const walker = require('walker') -const workdir = path.join(__dirname, 'workdir') -const appTemplateDir = path.join(__dirname, '..', 'templates', 'eject-ts') -const { eject } = require('../eject-ts') -const expected = {}; - -(function (cb) { - const files = [] - walker(appTemplateDir) - .on('file', function (file) { - files.push(file) - }) - .on('end', function () { - let count = 0 - files.forEach(function (file) { - readFile(file, function (err, data) { - if (err) { - return cb(err) - } - - expected[ - file.replace(appTemplateDir, '').replace(/__/, '.') - ] = data.toString() - - count++ - if (count === files.length) { - cb(null) - } - }) - }) - }) - .on('error', cb) -})(function (err) { - t.error(err) - define(t) -}) - -function define (t) { - const { beforeEach, test } = t - - beforeEach(() => { - rimraf.sync(workdir) - mkdirSync(workdir, { recursive: true }) - }) - - test('should finish succesfully with template', async (t) => { - try { - await eject(workdir) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - function verifyCopy (t, expected) { - return new Promise((resolve, reject) => { - let count = 0 - walker(workdir) - .on('file', function (file) { - count++ - try { - const data = readFileSync(file) - file = file.replace(workdir, '') - t.same( - data.toString().replace(/\r\n/g, '\n'), - expected[file], - file + ' matching' - ) - } catch (err) { - reject(err) - } - }) - .on('end', function () { - t.equal(Object.keys(expected).length, count) - resolve() - }) - .on('error', function (err, entry, stat) { - reject(err) - }) - }) - } -} diff --git a/test/eject.test.js b/test/eject.test.js deleted file mode 100644 index 6648fe34..00000000 --- a/test/eject.test.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -// bailout if a test is broken -// so that the folder can be inspected -process.env.TAP_BAIL = true - -const t = require('tap') -const { - mkdirSync, - readFileSync, - readFile -} = require('fs') -const path = require('path') -const rimraf = require('rimraf') -const walker = require('walker') -const workdir = path.join(__dirname, 'workdir') -const appTemplateDir = path.join(__dirname, '..', 'templates', 'eject') -const { eject } = require('../eject') -const expected = {} - -;(function (cb) { - const files = [] - walker(appTemplateDir) - .on('file', function (file) { - files.push(file) - }) - .on('end', function () { - let count = 0 - files.forEach(function (file) { - readFile(file, function (err, data) { - if (err) { - return cb(err) - } - - expected[file.replace(appTemplateDir, '').replace(/__/, '.')] = data.toString() - - count++ - if (count === files.length) { - cb(null) - } - }) - }) - }) - .on('error', cb) -})(function (err) { - t.error(err) - define(t) -}) - -function define (t) { - const { beforeEach, test } = t - - beforeEach(() => { - rimraf.sync(workdir) - mkdirSync(workdir, { recursive: true }) - }) - - test('should finish succesfully with template', async (t) => { - try { - await eject(workdir) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - function verifyCopy (t, expected) { - return new Promise((resolve, reject) => { - let count = 0 - walker(workdir) - .on('file', function (file) { - count++ - try { - const data = readFileSync(file) - file = file.replace(workdir, '') - t.same(data.toString().replace(/\r\n/g, '\n'), expected[file], file + ' matching') - } catch (err) { - reject(err) - } - }) - .on('end', function () { - t.equal(Object.keys(expected).length, count) - resolve() - }) - .on('error', function (err, entry, stat) { - reject(err) - }) - }) - } -} diff --git a/test/env.ts b/test/env.ts new file mode 100644 index 00000000..e2ee94ad --- /dev/null +++ b/test/env.ts @@ -0,0 +1,5 @@ +// TODO: it take too long for Github Actions to run npm install +export const INGORE_NPM_INSTALL = Boolean(process.env.IGNORE_NPM_INSTALL ?? true) +// TODO: it should ensure the generated project coverage +// currently, nyc is throwing in this apporach +export const INGORE_GENERATE_TEST = Boolean(process.env.INGORE_GENERATE_TEST ?? true) diff --git a/test/generate-plugin.test.js b/test/generate-plugin.test.js deleted file mode 100644 index 9e3db83c..00000000 --- a/test/generate-plugin.test.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict' - -// bailout if a test is broken -// so that the folder can be inspected -process.env.TAP_BAIL = true - -const t = require('tap') -const { - mkdirSync, - readFileSync, - readFile -} = require('fs') -const path = require('path') -const rimraf = require('rimraf') -const walker = require('walker') -const { generate, pluginTemplate } = require('../generate-plugin') -const workdir = path.join(__dirname, 'workdir') -const templateDir = path.join(__dirname, '..', 'templates', 'plugin') -const cliPkg = require('../package') -const { exec } = require('child_process') -const minimatch = require('minimatch') -const strip = require('strip-ansi') -const expected = {} - -;(function (cb) { - const files = [] - walker(templateDir) - .on('file', function (file) { - files.push(file) - }) - .on('end', function () { - let count = 0 - files.forEach(function (file) { - readFile(file, function (err, data) { - if (err) { - return cb(err) - } - - expected[file.replace(templateDir, '').replace(/__/, '.')] = data.toString() - - count++ - if (count === files.length) { - cb(null) - } - }) - }) - }) - .on('error', cb) -})(function (err) { - t.error(err) - define(t) -}) - -function define (t) { - const { beforeEach, test } = t - - beforeEach(() => { - rimraf.sync(workdir) - mkdirSync(workdir, { recursive: true }) - }) - - test('errors if directory exists', (t) => { - t.plan(2) - exec('node generate-plugin.js ./test/workdir', (err, stdout) => { - t.equal('directory ./test/workdir already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if generate doesn\'t have arguments', (t) => { - t.plan(2) - exec('node generate-plugin.js', (err, stdout) => { - t.equal('must specify a directory to \'fastify generate\'', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate .', (t) => { - t.plan(2) - exec('node generate-plugin.js .', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate ./', (t) => { - t.plan(2) - exec('node generate-plugin.js ./', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if folder exists', (t) => { - t.plan(2) - exec('node generate-plugin.js test', (err, stdout) => { - t.equal('directory test already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('should finish succesfully', async (t) => { - t.plan(19 + Object.keys(expected).length) - try { - await generate(workdir, pluginTemplate) - await verifyPkg(t) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - function verifyPkg (t) { - return new Promise((resolve, reject) => { - const pkgFile = path.join(workdir, 'package.json') - - readFile(pkgFile, function (err, data) { - err && reject(err) - const pkg = JSON.parse(data) - t.equal(pkg.name, 'workdir') - t.equal(pkg.main, 'index.js') - t.equal(pkg.types, 'index.d.ts') - // we are not checking author because it depends on global npm configs - t.equal(pkg.version, '1.0.0') - t.equal(pkg.description, '') - t.ok(pkg.license === 'MIT') - t.equal(pkg.scripts.lint, 'standard && npm run lint:typescript') - t.equal(pkg.scripts['lint:typescript'], 'ts-standard') - t.equal(pkg.scripts.test, 'npm run lint && npm run unit && npm run test:typescript') - t.equal(pkg.scripts['test:typescript'], 'tsd') - t.equal(pkg.scripts.unit, 'tap test/**/*.test.js') - t.equal(pkg.dependencies['fastify-plugin'], cliPkg.devDependencies['fastify-plugin']) - t.equal(pkg.devDependencies['@types/node'], cliPkg.devDependencies['@types/node']) - t.equal(pkg.devDependencies.fastify, cliPkg.devDependencies.fastify) - t.equal(pkg.devDependencies.standard, cliPkg.devDependencies.standard) - t.equal(pkg.devDependencies.tap, cliPkg.devDependencies.tap) - t.equal(pkg.devDependencies.tsd, cliPkg.devDependencies.tsd) - t.equal(pkg.devDependencies.typescript, cliPkg.devDependencies.typescript) - t.same(pkg.tsd, pluginTemplate.tsd) - - const testGlob = pkg.scripts.unit.split(' ')[1] - t.equal(minimatch.match(['test/more/test/here/ok.test.js'], testGlob).length, 1) - resolve() - }) - }) - } - - function verifyCopy (t, expected) { - const pkgFile = path.join(workdir, 'package.json') - const githubDir = path.join(workdir, '.github', 'workflows', 'ci.yml') - return new Promise((resolve, reject) => { - walker(workdir) - .on('file', function (file) { - if (file === pkgFile || file === githubDir) { - return - } - try { - const data = readFileSync(file) - file = file.replace(workdir, '') - t.same(data.toString().replace(/\r\n/g, '\n'), expected[file], file + ' matching') - } catch (err) { - reject(err) - } - }) - .on('end', function () { - resolve() - }) - .on('error', function (err, entry, stat) { - reject(err) - }) - }) - } -} diff --git a/test/generate-readme.test.js b/test/generate-readme.test.js deleted file mode 100644 index eeb81750..00000000 --- a/test/generate-readme.test.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const fs = require('fs') -const t = require('tap') -const rimraf = require('rimraf') -const { generate } = require('../generate-readme') - -const plugindir = path.join(__dirname, 'plugindir') -const plugin = require(plugindir) -const { test } = t - -test('should create readme', async (t) => { - t.plan(1) - const pluginMeta = plugin[Symbol.for('plugin-meta')] - const encapsulated = !plugin[Symbol.for('skip-override')] - const pluginFileName = path.basename(plugindir) - try { - await generate(plugindir, { pluginMeta, encapsulated, pluginFileName }) - const readme = path.join(plugindir, 'README.md') - t.ok(fs.existsSync(readme)) - rimraf(readme, () => t.end()) - } catch (err) { - t.error(err) - } -}) diff --git a/test/generate-typescript.test.js b/test/generate-typescript.test.js deleted file mode 100644 index e8ec1783..00000000 --- a/test/generate-typescript.test.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict' - -// bailout if a test is broken -// so that the folder can be inspected -process.env.TAP_BAIL = true - -const t = require('tap') -const { - mkdirSync, - readFileSync, - readFile -} = require('fs') -const path = require('path') -const rimraf = require('rimraf') -const walker = require('walker') -const { generate, typescriptTemplate } = require('../generate') -const workdir = path.join(__dirname, 'workdir') -const appTemplateDir = path.join(__dirname, '..', 'templates', 'app-ts') -const cliPkg = require('../package') -const { exec } = require('child_process') -const minimatch = require('minimatch') -const strip = require('strip-ansi') -const expected = {} - -;(function (cb) { - const files = [] - walker(appTemplateDir) - .on('file', function (file) { - files.push(file) - }) - .on('end', function () { - let count = 0 - files.forEach(function (file) { - readFile(file, function (err, data) { - if (err) { - return cb(err) - } - - expected[file.replace(appTemplateDir, '').replace(/__/, '.')] = data.toString() - - count++ - if (count === files.length) { - cb(null) - } - }) - }) - }) - .on('error', cb) -})(function (err) { - t.error(err) - define(t) -}) - -function define (t) { - const { beforeEach, test } = t - - beforeEach(() => { - rimraf.sync(workdir) - mkdirSync(workdir, { recursive: true }) - }) - - test('errors if directory exists', (t) => { - t.plan(2) - exec('node generate.js --lang=ts ./test/workdir', (err, stdout) => { - t.equal('directory ./test/workdir already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if generate doesn\'t have arguments', (t) => { - t.plan(2) - exec('node generate.js --lang=ts', (err, stdout) => { - t.equal('must specify a directory to \'fastify generate\'', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate .', (t) => { - t.plan(2) - exec('node generate.js --lang=ts .', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate ./', (t) => { - t.plan(2) - exec('node generate.js --lang=ts ./', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if folder exists', (t) => { - t.plan(2) - exec('node generate.js --lang=ts test', (err, stdout) => { - t.equal('directory test already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('should finish successfully with typescript template', async (t) => { - t.plan(25 + Object.keys(expected).length) - try { - await generate(workdir, typescriptTemplate) - await verifyPkg(t) - await verifyTSConfig(t) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - function verifyPkg (t) { - return new Promise((resolve, reject) => { - const pkgFile = path.join(workdir, 'package.json') - - readFile(pkgFile, function (err, data) { - t.error(err) - const pkg = JSON.parse(data) - t.equal(pkg.name, 'workdir') - // we are not checking author because it depends on global npm configs - t.equal(pkg.version, '1.0.0') - t.equal(pkg.description, 'This project was bootstrapped with Fastify-CLI.') - // by default this will be ISC but since we have a MIT licensed pkg file in upper dir, npm will set the license to MIT in this case - // so for local tests we need to accept MIT as well - t.ok(pkg.license === 'ISC' || pkg.license === 'MIT') - t.equal(pkg.scripts.test, 'npm run build:ts && tsc -p test/tsconfig.json && tap --ts test/**/*.test.ts') - t.equal(pkg.scripts.start, 'npm run build:ts && fastify start -l info dist/app.js') - t.equal(pkg.scripts['build:ts'], 'tsc') - t.equal(pkg.scripts['watch:ts'], 'tsc -w') - t.equal(pkg.scripts.dev, 'npm run build:ts && concurrently -k -p "[{name}]" -n "TypeScript,App" -c "yellow.bold,cyan.bold" "npm:watch:ts" "npm:dev:start"') - t.equal(pkg.scripts['dev:start'], 'fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js') - t.equal(pkg.dependencies['fastify-cli'], '^' + cliPkg.version) - t.equal(pkg.dependencies.fastify, cliPkg.dependencies.fastify) - t.equal(pkg.dependencies['fastify-plugin'], cliPkg.devDependencies['fastify-plugin'] || cliPkg.dependencies['fastify-plugin']) - t.equal(pkg.dependencies['@fastify/autoload'], cliPkg.devDependencies['@fastify/autoload']) - t.equal(pkg.dependencies['@fastify/sensible'], cliPkg.devDependencies['@fastify/sensible']) - t.equal(pkg.devDependencies['@types/node'], cliPkg.devDependencies['@types/node']) - t.equal(pkg.devDependencies['ts-node'], cliPkg.devDependencies['ts-node']) - t.equal(pkg.devDependencies.concurrently, cliPkg.devDependencies.concurrently) - t.equal(pkg.devDependencies.tap, cliPkg.devDependencies.tap) - t.equal(pkg.devDependencies.typescript, cliPkg.devDependencies.typescript) - - const testGlob = pkg.scripts.test.split(' ')[10] - - t.equal(minimatch.match(['test/routes/plugins/more/test/here/ok.test.ts'], testGlob).length, 1) - resolve() - }) - }) - } - - function verifyTSConfig (t) { - const tsConfigFile = path.join(workdir, 'tsconfig.json') - - readFile(tsConfigFile, function (err, data) { - t.error(err) - const tsConfig = JSON.parse(data) - - t.equal(tsConfig.extends, 'fastify-tsconfig') - t.equal(tsConfig.compilerOptions.outDir, 'dist') - t.same(tsConfig.include, ['src/**/*.ts']) - }) - } - - function verifyCopy (t, expected) { - const pkgFile = path.join(workdir, 'package.json') - const tsConfigFile = path.join(workdir, 'tsconfig.json') - return new Promise((resolve, reject) => { - walker(workdir) - .on('file', function (file) { - if (file === pkgFile || file === tsConfigFile) { - return - } - try { - const data = readFileSync(file) - file = file.replace(workdir, '') - t.same(data.toString().replace(/\r\n/g, '\n'), expected[file], file + ' matching') - } catch (err) { - reject(err) - } - }) - .on('end', function () { - resolve() - }) - .on('error', function (err, entry, stat) { - reject(err) - }) - }) - } -} diff --git a/test/generate.test.js b/test/generate.test.js deleted file mode 100644 index 1d8b14c2..00000000 --- a/test/generate.test.js +++ /dev/null @@ -1,186 +0,0 @@ -'use strict' - -// bailout if a test is broken -// so that the folder can be inspected -process.env.TAP_BAIL = true - -const t = require('tap') -const { - mkdirSync, - readFileSync, - readFile, - unlink -} = require('fs') -const path = require('path') -const { promisify } = require('util') -const rimraf = require('rimraf') -const walker = require('walker') -const { generate, javascriptTemplate } = require('../generate') -const workdir = path.join(__dirname, 'workdir') -const appTemplateDir = path.join(__dirname, '..', 'templates', 'app') -const cliPkg = require('../package') -const { exec } = require('child_process') -const pExec = promisify(exec) -const pUnlink = promisify(unlink) -const minimatch = require('minimatch') -const strip = require('strip-ansi') -const expected = {} - -;(function (cb) { - const files = [] - walker(appTemplateDir) - .on('file', function (file) { - files.push(file) - }) - .on('end', function () { - let count = 0 - files.forEach(function (file) { - readFile(file, function (err, data) { - if (err) { - return cb(err) - } - - expected[file.replace(appTemplateDir, '').replace(/__/, '.')] = data.toString() - - count++ - if (count === files.length) { - cb(null) - } - }) - }) - }) - .on('error', cb) -})(function (err) { - t.error(err) - define(t) -}) - -function define (t) { - const { beforeEach, test } = t - - beforeEach(() => { - rimraf.sync(workdir) - mkdirSync(workdir, { recursive: true }) - }) - - test('errors if directory exists', (t) => { - t.plan(2) - exec('node generate.js ./test/workdir', (err, stdout) => { - t.equal('directory ./test/workdir already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if generate doesn\'t have arguments', (t) => { - t.plan(2) - exec('node generate.js', (err, stdout) => { - t.equal('must specify a directory to \'fastify generate\'', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate . and integrate flag is not set', (t) => { - t.plan(2) - exec('node generate.js .', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if package.json exists when use generate ./ and integrate flag is not set', (t) => { - t.plan(2) - exec('node generate.js ./', (err, stdout) => { - t.equal('a package.json file already exists in target directory', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('errors if folder exists', (t) => { - t.plan(2) - exec('node generate.js test', (err, stdout) => { - t.equal('directory test already exists', strip(stdout.toString().trim())) - t.equal(1, err.code) - }) - }) - - test('should finish succesfully with javascript template', async (t) => { - t.plan(14 + Object.keys(expected).length) - try { - await generate(workdir, javascriptTemplate) - await verifyPkg(t) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - test('--integrate option will enhance preexisting package.json and overwrite preexisting files', async (t) => { - t.plan(14 + Object.keys(expected).length) - try { - await generate(workdir, javascriptTemplate) - await pUnlink(path.join(workdir, 'package.json')) - await pExec('npm init -y', { cwd: workdir }) - await pExec('node ../../generate . --integrate', { cwd: workdir }) - await verifyPkg(t) - await verifyCopy(t, expected) - } catch (err) { - t.error(err) - } - }) - - function verifyPkg (t) { - return new Promise((resolve, reject) => { - const pkgFile = path.join(workdir, 'package.json') - - readFile(pkgFile, function (err, data) { - err && reject(err) - const pkg = JSON.parse(data) - t.equal(pkg.name, 'workdir') - // we are not checking author because it depends on global npm configs - t.equal(pkg.version, '1.0.0') - t.equal(pkg.description, 'This project was bootstrapped with Fastify-CLI.') - // by default this will be ISC but since we have a MIT licensed pkg file in upper dir, npm will set the license to MIT in this case - // so for local tests we need to accept MIT as well - t.ok(pkg.license === 'ISC' || pkg.license === 'MIT') - t.equal(pkg.scripts.test, 'tap "test/**/*.test.js"') - t.equal(pkg.scripts.start, 'fastify start -l info app.js') - t.equal(pkg.scripts.dev, 'fastify start -w -l info -P app.js') - t.equal(pkg.dependencies['fastify-cli'], '^' + cliPkg.version) - t.equal(pkg.dependencies.fastify, cliPkg.dependencies.fastify) - t.equal(pkg.dependencies['fastify-plugin'], cliPkg.devDependencies['fastify-plugin'] || cliPkg.dependencies['fastify-plugin']) - t.equal(pkg.dependencies['@fastify/autoload'], cliPkg.devDependencies['@fastify/autoload']) - t.equal(pkg.dependencies['@fastify/sensible'], cliPkg.devDependencies['@fastify/sensible']) - t.equal(pkg.devDependencies.tap, cliPkg.devDependencies.tap) - - const testGlob = pkg.scripts.test.split(' ')[1].replace(/"/g, '') - t.equal(minimatch.match(['test/services/plugins/more/test/here/ok.test.js'], testGlob).length, 1) - resolve() - }) - }) - } - - function verifyCopy (t, expected) { - const pkgFile = path.join(workdir, 'package.json') - return new Promise((resolve, reject) => { - walker(workdir) - .on('file', function (file) { - if (file === pkgFile) { - return - } - try { - const data = readFileSync(file) - file = file.replace(workdir, '') - t.same(data.toString().replace(/\r\n/g, '\n'), expected[file], file + ' matching') - } catch (err) { - reject(err) - } - }) - .on('end', function () { - resolve() - }) - .on('error', function (err, entry, stat) { - reject(err) - }) - }) - } -} diff --git a/test/graceful-shutdown.test.js b/test/graceful-shutdown.test.js deleted file mode 100644 index 4b9196e0..00000000 --- a/test/graceful-shutdown.test.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict' - -const t = require('tap') -// Tests skip on win32 platforms due SIGINT signal is not supported across all windows platforms -const test = (process.platform === 'win32') ? t.skip : t.test -const sinon = require('sinon') -const start = require('../start') - -let _port = 3001 - -function getPort () { - return '' + _port++ -} - -let spy = null -let fastify = null -let signalCounter = null -const sandbox = sinon.createSandbox() - -t.beforeEach(async () => { - signalCounter = process.listenerCount('SIGINT') - - const argv = ['-p', getPort(), './examples/plugin.js'] - fastify = await start.start(argv) - spy = sinon.spy(fastify, 'close') -}) - -t.afterEach(async () => { - sandbox.restore() -}) - -test('should add and remove SIGINT listener as expected ', async t => { - t.plan(2) - - t.equal(process.listenerCount('SIGINT'), signalCounter + 1) - - await fastify.close() - - t.equal(process.listenerCount('SIGINT'), signalCounter) - - t.end() -}) - -test('should have called fastify.close() when receives a SIGINT signal', async t => { - process.once('SIGINT', () => { - sinon.assert.called(spy) - - t.end() - - process.exit() - }) - - process.kill(process.pid, 'SIGINT') -}) diff --git a/test/helper.test.js b/test/helper.test.js deleted file mode 100644 index cdcc5aa2..00000000 --- a/test/helper.test.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict' - -const util = require('util') -const fs = require('fs') -const path = require('path') -const { test } = require('tap') - -const helper = require('../helper') - -const writeFile = util.promisify(fs.writeFile) -const readFile = util.promisify(fs.readFile) - -test('should return the fastify instance', async t => { - const argv = ['./examples/plugin.js'] - const app = await helper.build(argv, {}) - t.teardown(() => app.close()) - t.notOk(app.server.listening) -}) - -test('should reload the env at each build', async t => { - const testdir = t.testdir({ - '.env': 'GREETING=world', - 'plugin.js': await readFile(path.join(__dirname, '../examples/plugin-with-env.js')) - }) - - const argv = [path.join(testdir, 'plugin.js')] - const cwd = process.cwd() - - process.chdir(testdir) - t.teardown(() => { process.chdir(cwd) }) - - { - await writeFile(path.join(testdir, '.env'), 'GREETING=one') - const app = await helper.build(argv) - t.teardown(() => app.close()) - const res = await app.inject('/') - t.same(res.json(), { hello: 'one' }) - } - - { - delete process.env.GREETING // dotenv will not overwrite the env if set - await writeFile(path.join(testdir, '.env'), 'GREETING=two') - const app = await helper.build(argv) - t.teardown(() => app.close()) - const res = await app.inject('/') - t.same(res.json(), { hello: 'two' }) - } -}) - -test('setting plugin options', async t => { - const argv = [ - './examples/plugin-with-custom-options.js', - '--', - '-abc', - '--hello', - 'world' - ] - const app = await helper.build(argv, { from: 'build' }) - t.teardown(() => app.close()) - const res = await app.inject('/') - t.same(res.json(), { - a: true, - b: true, - c: true, - hello: 'world', - from: 'build' - }) -}) - -test('setting plugin options, extra has priority', async t => { - const argv = [ - './examples/plugin-with-custom-options.js', - '--', - '--hello', - 'world' - ] - const app = await helper.build(argv, { hello: 'planet' }) - t.teardown(() => app.close()) - const res = await app.inject('/') - t.same(res.json(), { - hello: 'planet' - }) -}) - -test('setting plugin options, extra has priority', async t => { - const args = './examples/plugin-with-custom-options.js -- --hello world --from args' - const app = await helper.build(args, { hello: 'planet' }) - t.teardown(() => app.close()) - const res = await app.inject('/') - t.same(res.json(), { - hello: 'planet', - from: 'args' - }) -}) - -test('should start fastify', async t => { - const argv = ['./examples/plugin.js'] - const app = await helper.listen(argv, {}) - t.teardown(() => app.close()) - t.ok(app.server.listening) -}) diff --git a/test/plugindir/package.json b/test/plugindir/package.json deleted file mode 100644 index f680eaa3..00000000 --- a/test/plugindir/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "plugindir", - "version": "1.0.0", - "description": "test for generator-readme", - "main": "plugin.js", - "scripts": { - "test": "" - }, - "dependencies": { - "fastify-plugin": "^1.4.0" - }, - "author": "", - "license": "ISC" -} diff --git a/test/plugindir/plugin.js b/test/plugindir/plugin.js deleted file mode 100644 index c2232ab5..00000000 --- a/test/plugindir/plugin.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict' - -const fp = require('fastify-plugin') - -module.exports = fp(function (fastify, opts, next) { - fastify.decorate('someSupport', function () { - return 'hugs' - }) - next() -}) diff --git a/test/print-routes.test.js b/test/print-routes.test.js deleted file mode 100644 index 88b86132..00000000 --- a/test/print-routes.test.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict' - -const proxyquire = require('proxyquire') -const tap = require('tap') -const sinon = require('sinon') - -const printRoutes = require('../print-routes') - -const test = tap.test - -test('should print routes', async t => { - t.plan(2) - - const spy = sinon.spy() - const command = proxyquire('../print-routes', { - './log': spy - }) - const fastify = await command.printRoutes(['./examples/plugin.js']) - - await fastify.close() - t.ok(spy.called) - t.ok(spy.calledWithMatch('debug', '└── / (GET)\n / (POST)\n')) -}) - -test('should print routes via cli', async t => { - t.plan(2) - - const spy = sinon.spy() - const command = proxyquire('../print-routes', { - './log': spy - }) - await command.cli(['./examples/plugin.js']) - - t.ok(spy.called) - t.ok(spy.calledWithMatch('debug', '└── / (GET)\n / (POST)\n')) -}) - -test('should warn on file not found', t => { - t.plan(1) - - const oldStop = printRoutes.stop - t.teardown(() => { printRoutes.stop = oldStop }) - printRoutes.stop = function (message) { - t.ok(/.*not-found.js doesn't exist within/.test(message), message) - } - - const argv = ['./data/not-found.js'] - printRoutes.printRoutes(argv) -}) - -test('should throw on package not found', t => { - t.plan(1) - - const oldStop = printRoutes.stop - t.teardown(() => { printRoutes.stop = oldStop }) - printRoutes.stop = function (err) { - t.ok(/Cannot find module 'unknown-package'/.test(err.message), err.message) - } - - const argv = ['./test/data/package-not-found.js'] - printRoutes.printRoutes(argv) -}) - -test('should throw on parsing error', t => { - t.plan(1) - - const oldStop = printRoutes.stop - t.teardown(() => { printRoutes.stop = oldStop }) - printRoutes.stop = function (err) { - t.equal(err.constructor, SyntaxError) - } - - const argv = ['./test/data/parsing-error.js'] - printRoutes.printRoutes(argv) -}) - -test('should exit without error on help', t => { - const exit = process.exit - process.exit = sinon.spy() - - t.teardown(() => { - process.exit = exit - }) - - const argv = ['-h', 'true'] - printRoutes.printRoutes(argv) - - t.ok(process.exit.called) - t.equal(process.exit.lastCall.args[0], undefined) - - t.end() -}) - -test('should print routes of server with an async/await plugin', async t => { - const nodeMajorVersion = process.versions.node.split('.').map(x => parseInt(x, 10))[0] - if (nodeMajorVersion < 7) { - t.pass('Skip because Node version < 7') - return t.end() - } - - t.plan(2) - - const spy = sinon.spy() - const command = proxyquire('../print-routes', { - './log': spy - }) - const argv = ['./examples/async-await-plugin.js'] - const fastify = await command.printRoutes(argv) - - await fastify.close() - t.ok(spy.called) - t.ok(spy.calledWithMatch('debug', '└── / (GET)\n')) -}) diff --git a/test/run-command.ts b/test/run-command.ts new file mode 100644 index 00000000..c126485b --- /dev/null +++ b/test/run-command.ts @@ -0,0 +1,116 @@ +import { ChildProcessWithoutNullStreams, spawn, SpawnOptionsWithoutStdio } from 'child_process' +import { Writable } from 'stream' +import { sleep } from './sleep' + +export const ENTER = '\n' +export const ESC = '\u001B[' +export const KEY_UP = `${ESC}1A` +export const KEY_DOWN = `${ESC}1B` + +export async function press (stream: Writable, keycode: string): Promise { + stream.write(keycode) + await sleep(100) +} + +export async function until (stream: CustomWritable, line: string | RegExp | ((line: string) => boolean)): Promise { + let found = false + const match = compileMatch(line) + while (!found) { + await sleep(100) + + const index = stream.lines.findIndex(match) + if (index !== -1) found = true + } + // we empty the buffer after found + stream.lines = [] +} + +export function compileMatch (line: string | RegExp | ((line: string) => boolean)): ((line: string) => boolean) { + if (typeof line === 'string') { + return function match (from: string) { + return from === line + } + } + if (line instanceof RegExp) { + return function match (from: string) { + return line.test(from) + } + } + return line +} + +export class CustomWritable extends Writable { + lines: string[] = [] + + _write (chunk: any, encoding: BufferEncoding, done: (error?: Error) => void): void { + this.lines.push(chunk.toString()) + done() + } + + async until (line: string | RegExp | ((line: string) => boolean)): Promise { + return await until(this, line) + } +} + +interface CustomStdIn extends Writable { + press: (keycode: string) => Promise + writeLn: (line: string) => Promise +} + +export function runRawCommand (args: string[] = [], options: SpawnOptionsWithoutStdio = {}): { + stdout: CustomWritable + stderr: CustomWritable + stdin: CustomStdIn + child: ChildProcessWithoutNullStreams + exited: Promise +} { + if (args.length < 1) throw new Error('args expected to be length greater than zero.') + const child = spawn(args.shift() as string, args, options) + const stderr = new CustomWritable() + const stdout = new CustomWritable() + const stdin = child.stdin as CustomStdIn + child.stdout.pipe(stdout) + child.stderr.pipe(stderr) + let isExit = false + + child.once('exit', () => { + isExit = true + stdout.end() + stderr.end() + }) + + stdin.press = async function (keycode: string): Promise { + stdin.write(keycode) + await sleep(100) + } + + stdin.writeLn = async function (line: string): Promise { + stdin.write(`${line}${ENTER}`) + await sleep + } + + return { + stdout, + stdin, + stderr, + child, + exited: new Promise((resolve) => { + const interval = setInterval(() => { + if (isExit) { + clearInterval(interval) + resolve() + } + }, 100) + }) + } +} + +export function runCommand (args: string[] = [], options: SpawnOptionsWithoutStdio = {}): { + stdout: CustomWritable + stderr: CustomWritable + stdin: CustomStdIn + child: ChildProcessWithoutNullStreams + exited: Promise +} { + return runRawCommand(['node', 'bin/run', ...args], options) +} diff --git a/test/sleep.ts b/test/sleep.ts new file mode 100644 index 00000000..7399bd0d --- /dev/null +++ b/test/sleep.ts @@ -0,0 +1,5 @@ +export async function sleep (ms: number): Promise { + return await new Promise(function (resolve) { + setTimeout(resolve, ms) + }) +} diff --git a/test/start.test.js b/test/start.test.js deleted file mode 100644 index 13594775..00000000 --- a/test/start.test.js +++ /dev/null @@ -1,983 +0,0 @@ -/* global GLOBAL_MODULE_1, GLOBAL_MODULE_2 */ -'use strict' - -const util = require('util') -const { once } = require('events') -const fs = require('fs') -const path = require('path') -const crypto = require('crypto') -const semver = require('semver') -const baseFilename = path.join(__dirname, 'fixtures', `test_${crypto.randomBytes(16).toString('hex')}`) -const { fork } = require('child_process') -const moduleSupport = semver.satisfies(process.version, '>= 14 || >= 12.17.0 < 13.0.0') - -const t = require('tap') -const test = t.test -const sgetOriginal = require('simple-get').concat -const sget = (opts, cb) => { - return new Promise((resolve, reject) => { - sgetOriginal(opts, (err, response, body) => { - if (err) return reject(err) - return resolve({ response, body }) - }) - }) -} -const sinon = require('sinon') -const proxyquire = require('proxyquire').noPreserveCache() -const start = require('../start') - -const onGithubAction = !!process.env.GITHUB_ACTION -const writeFile = util.promisify(fs.writeFile) -const readFile = util.promisify(fs.readFile) - -function requireUncached (module) { - delete require.cache[require.resolve(module)] - return require(module) -} - -let _port = 3001 - -function getPort () { - return '' + _port++ -} - -// FIXME -// paths are relative to the root of the project -// this can be run only from there - -test('should start the server', async t => { - t.plan(4) - - const argv = ['-p', getPort(), './examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server with a typescript compiled module', async t => { - t.plan(4) - - const argv = ['-p', getPort(), './examples/ts-plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server with pretty output', async t => { - t.plan(4) - - const argv = ['-p', getPort(), '-P', './examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start fastify with custom options', async t => { - t.plan(1) - // here the test should fail because of the wrong certificate - // or because the server is booted without the custom options - try { - const argv = ['-p', getPort(), '-o', 'true', './examples/plugin-with-options.js'] - const fastify = await start.start(argv) - await fastify.close() - t.pass('server closed') - } catch (e) { - t.pass('Custom options') - } -}) - -test('should start fastify with custom plugin options', async t => { - t.plan(4) - - const argv = [ - '-p', - getPort(), - './examples/plugin-with-custom-options.js', - '--', - '-abc', - '--hello', - 'world' - ] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { - a: true, - b: true, - c: true, - hello: 'world' - }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start fastify with custom options with a typescript compiled plugin', async t => { - t.plan(1) - // here the test should fail because of the wrong certificate - // or because the server is booted without the custom options - try { - const argv = ['-p', getPort(), '-o', 'true', './examples/ts-plugin-with-options.js'] - await start.start(argv) - t.fail('Custom options') - } catch (e) { - t.pass('Custom options') - } -}) - -test('should start fastify with custom plugin options with a typescript compiled plugin', async t => { - t.plan(4) - - const argv = [ - '-p', - getPort(), - './examples/ts-plugin-with-custom-options.js', - '--', - '-abc', - '--hello', - 'world' - ] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { - a: true, - b: true, - c: true, - hello: 'world' - }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server at the given prefix', async t => { - t.plan(4) - - const argv = ['-p', getPort(), '-x', '/api/hello', './examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}/api/hello` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - await fastify.close() - t.pass('server closed') -}) - -test('should start fastify at given socket path', { skip: process.platform === 'win32' }, async t => { - t.plan(1) - - const sockFile = path.resolve('test.sock') - t.teardown(() => { - try { - fs.unlinkSync(sockFile) - } catch (e) { } - }) - const argv = ['-s', sockFile, '-o', 'true', './examples/plugin.js'] - - try { - fs.unlinkSync(sockFile) - } catch (e) { } - - const fastify = await start.start(argv) - - await new Promise((resolve, reject) => { - const request = require('http').request({ - method: 'GET', - path: '/', - socketPath: sockFile - }, function (response) { - t.same(response.statusCode, 200) - return resolve() - }) - request.end() - }) - - t.teardown(fastify.close.bind(fastify)) -}) - -test('should error with a good timeout value', async t => { - t.plan(1) - - const start = proxyquire('../start', { - assert: { - ifError (err) { - t.equal(err.code, 'AVV_ERR_READY_TIMEOUT') - } - } - }) - - try { - const argv = ['-p', '3040', '-T', '100', './test/data/timeout-plugin.js'] - await start.start(argv) - } catch (err) { - t.equal(err.code, 'AVV_ERR_READY_TIMEOUT') - } -}) - -test('should warn on file not found', t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (message) { - t.ok(/.*not-found.js doesn't exist within/.test(message), message) - } - - const argv = ['-p', getPort(), './data/not-found.js'] - start.start(argv) -}) - -test('should throw on package not found', t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/Cannot find module 'unknown-package'/.test(err.message), err.message) - } - - const argv = ['-p', getPort(), './test/data/package-not-found.js'] - start.start(argv) -}) - -test('should throw on parsing error', t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.equal(err.constructor, SyntaxError) - } - - const argv = ['-p', getPort(), './test/data/parsing-error.js'] - start.start(argv) -}) - -test('should start the server with an async/await plugin', async t => { - if (Number(process.versions.node[0]) < 7) { - t.pass('Skip because Node version < 7') - return t.end() - } - - t.plan(4) - - const argv = ['-p', getPort(), './examples/async-await-plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - await fastify.close() - t.pass('server closed') -}) - -test('should exit without error on help', t => { - const exit = process.exit - process.exit = sinon.spy() - - t.teardown(() => { - process.exit = exit - }) - - const argv = ['-p', getPort(), '-h', 'true'] - start.start(argv) - - t.ok(process.exit.called) - t.equal(process.exit.lastCall.args[0], undefined) - - t.end() -}) - -test('should throw the right error on require file', t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/undefinedVariable is not defined/.test(err.message), err.message) - } - - const argv = ['-p', getPort(), './test/data/undefinedVariable.js'] - start.start(argv) -}) - -test('should respond 413 - Payload too large', async t => { - t.plan(3) - - const bodyTooLarge = '{1: 11}' - const bodySmaller = '{1: 1}' - - const bodyLimitValue = '' + (bodyTooLarge.length + 2 - 1) - const argv = ['-p', getPort(), '--body-limit', bodyLimitValue, './examples/plugin.js'] - const fastify = await start.start(argv) - - const { response: responseFail } = await sget({ - method: 'POST', - url: `http://localhost:${fastify.server.address().port}`, - body: bodyTooLarge, - json: true - }) - - t.equal(responseFail.statusCode, 413) - - const { response: responseOk } = await sget({ - method: 'POST', - url: `http://localhost:${fastify.server.address().port}`, - body: bodySmaller, - json: true - }) - t.equal(responseOk.statusCode, 200) - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server (using env var)', async t => { - t.plan(4) - - process.env.FASTIFY_PORT = getPort() - const argv = ['./examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${process.env.FASTIFY_PORT}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - delete process.env.FASTIFY_PORT - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server (using PORT-env var)', async t => { - t.plan(4) - - process.env.PORT = getPort() - const argv = ['./examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${process.env.PORT}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - delete process.env.PORT - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server (using FASTIFY_PORT-env preceding PORT-env var)', async t => { - t.plan(4) - - process.env.FASTIFY_PORT = getPort() - process.env.PORT = getPort() - const argv = ['./examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${process.env.FASTIFY_PORT}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - delete process.env.FASTIFY_PORT - delete process.env.PORT - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server (using -p preceding FASTIFY_PORT-env var)', async t => { - t.plan(4) - - const port = getPort() - process.env.FASTIFY_PORT = getPort() - const argv = ['-p', port, './examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - delete process.env.FASTIFY_PORT - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server at the given prefix (using env var)', async t => { - t.plan(4) - - process.env.FASTIFY_PORT = getPort() - process.env.FASTIFY_PREFIX = '/api/hello' - const argv = ['./examples/plugin.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${process.env.FASTIFY_PORT}/api/hello` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hello: 'world' }) - - delete process.env.FASTIFY_PORT - delete process.env.FASTIFY_PREFIX - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server at the given prefix (using env var read from dotenv)', async t => { - t.plan(3) - - const start = proxyquire('../start', { - dotenv: { - config () { - t.pass('config called') - process.env.FASTIFY_PORT = 8080 - } - } - }) - const argv = ['./examples/plugin.js'] - const fastify = await start.start(argv) - t.equal(fastify.server.address().port, 8080) - delete process.env.FASTIFY_PORT - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server listening on 0.0.0.0 when running in docker', async t => { - t.plan(2) - const isDocker = sinon.stub() - isDocker.returns(true) - - const start = proxyquire('../start', { - 'is-docker': isDocker - }) - - const argv = ['-p', getPort(), './examples/plugin.js'] - const fastify = await start.start(argv) - - t.equal(fastify.server.address().address, '0.0.0.0') - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server with watch options that the child process restart when directory changed', { skip: onGithubAction }, async (t) => { - t.plan(4) - const tmpjs = path.resolve(baseFilename + '.js') - - await writeFile(tmpjs, 'hello world') - const argv = ['-p', '4042', '-w', './examples/plugin.js'] - const fastifyEmitter = await start.start(argv) - - t.teardown(async () => { - if (fs.existsSync(tmpjs)) { - fs.unlinkSync(tmpjs) - } - await fastifyEmitter.stop() - }) - - await once(fastifyEmitter, 'start') - t.pass('should receive start event') - - await once(fastifyEmitter, 'ready') - t.pass('should receive ready event') - - await writeFile(tmpjs, 'hello fastify', { flag: 'a+' }) // chokidar watch can't catch change event in CI, but local test is all ok. you can remove annotation in local environment. - t.pass('change tmpjs') - - // this might happen more than once but does not matter in this context - await once(fastifyEmitter, 'restart') - t.pass('should receive restart event') -}) - -test('should start the server with watch and verbose-watch options that the child process restart when directory changed with console message about changes ', { skip: onGithubAction }, async (t) => { - t.plan(5) - - const spy = sinon.spy() - const watch = proxyquire('../lib/watch', { - './utils': { - logWatchVerbose: spy - } - }) - - const start = proxyquire('../start', { - './lib/watch': watch - }) - - const tmpjs = path.resolve(baseFilename + '.js') - - await writeFile(tmpjs, 'hello world') - const argv = ['-p', '4042', '-w', '--verbose-watch', './examples/plugin.js'] - const fastifyEmitter = await start.start(argv) - - t.teardown(async () => { - if (fs.existsSync(tmpjs)) { - fs.unlinkSync(tmpjs) - } - await fastifyEmitter.stop() - }) - - await once(fastifyEmitter, 'start') - t.pass('should receive start event') - - await once(fastifyEmitter, 'ready') - t.pass('should receive ready event') - - await writeFile(tmpjs, 'hello fastify', { flag: 'a+' }) // chokidar watch can't catch change event in CI, but local test is all ok. you can remove annotation in local environment. - t.pass('change tmpjs') - - // this might happen more than once but does not matter in this context - await once(fastifyEmitter, 'restart') - t.pass('should receive restart event') - - t.ok(spy.calledOnce, 'should print a console message on file update') -}) - -test('should reload the env on restart when watching', { skip: onGithubAction }, async (t) => { - const testdir = t.testdir({ - '.env': 'GREETING=world', - 'plugin.js': await readFile(path.join(__dirname, '../examples/plugin-with-env.js')) - }) - - const cwd = process.cwd() - - process.chdir(testdir) - - const port = getPort() - const argv = ['-p', port, '-w', path.join(testdir, 'plugin.js')] - const fastifyEmitter = await requireUncached('../start').start(argv) - - t.teardown(() => { - process.chdir(cwd) - }) - - await once(fastifyEmitter, 'ready') - - const r1 = await sget({ - method: 'GET', - url: `http://localhost:${port}` - }) - - t.equal(r1.response.statusCode, 200) - t.same(JSON.parse(r1.body), { hello: 'world' }) - - await writeFile(path.join(testdir, '.env'), 'GREETING=planet') - - await once(fastifyEmitter, 'restart') - - const r2 = await sget({ - method: 'GET', - url: `http://localhost:${port}` - }) - - t.equal(r2.response.statusCode, 200) - t.same(JSON.parse(r2.body), { hello: 'planet' }) - - await fastifyEmitter.stop() -}) - -test('should read env variables from .env file', async (t) => { - const port = getPort() - - const testdir = t.testdir({ - '.env': `FASTIFY_PORT=${port}`, - 'plugin.js': await readFile(path.join(__dirname, '../examples/plugin.js')) - }) - - const cwd = process.cwd() - - process.chdir(testdir) - - t.teardown(() => { - process.chdir(cwd) - }) - - const fastify = await requireUncached('../start').start([path.join(testdir, 'plugin.js')]) - t.equal(fastify.server.address().port, +port) - - const res = await sget({ - method: 'GET', - url: `http://localhost:${port}` - }) - - t.equal(res.response.statusCode, 200) - t.same(JSON.parse(res.body), { hello: 'world' }) - - await fastify.close() -}) - -test('crash on unhandled rejection', t => { - t.plan(1) - - const argv = ['-p', getPort(), './test/data/rejection.js'] - const child = fork(path.join(__dirname, '..', 'start.js'), argv, { silent: true }) - child.on('close', function (code) { - t.equal(code, 1) - }) -}) - -test('should start the server with inspect options and the defalut port is 9320', async t => { - t.plan(3) - - const start = proxyquire('../start', { - inspector: { - open (p) { - t.equal(p, 9320) - t.pass('inspect open called') - } - } - }) - const argv = ['--d', './examples/plugin.js'] - const fastify = await start.start(argv) - - await fastify.close() - t.pass('server closed') -}) - -test('should start the server with inspect options and use the exactly port', async t => { - t.plan(3) - - const port = getPort() - const start = proxyquire('../start', { - inspector: { - open (p) { - t.equal(p, Number(port)) - t.pass('inspect open called') - } - } - }) - const argv = ['--d', '--debug-port', port, './examples/plugin.js'] - const fastify = await start.start(argv) - - await fastify.close() - t.pass('server closed') -}) - -test('boolean env are not overridden if no arguments are passed', async t => { - t.plan(1) - - process.env.FASTIFY_OPTIONS = 'true' - - // here the test should fail because of the wrong certificate - // or because the server is booted without the custom options - try { - const argv = ['./examples/plugin-with-options.js'] - await start.start(argv) - t.fail('Custom options') - } catch (e) { - t.pass('Custom options') - } -}) - -test('should support preloading custom module', async t => { - t.plan(2) - - const argv = ['-r', './test/data/custom-require.js', './examples/plugin.js'] - const fastify = await start.start(argv) - t.ok(GLOBAL_MODULE_1) - - await fastify.close() - t.pass('server closed') -}) - -test('should support preloading multiple custom modules', async t => { - t.plan(3) - - const argv = ['-r', './test/data/custom-require.js', '-r', './test/data/custom-require2.js', './examples/plugin.js'] - const fastify = await start.start(argv) - t.ok(GLOBAL_MODULE_1) - t.ok(GLOBAL_MODULE_2) - - await fastify.close() - t.pass('server closed') -}) - -test('preloading custom module with empty and trailing require flags should not throw', async t => { - t.plan(2) - - const argv = ['-r', './test/data/custom-require.js', '-r', '', './examples/plugin.js', '-r'] - const fastify = await start.start(argv) - t.ok(GLOBAL_MODULE_1) - - await fastify.close() - t.pass('server closed') -}) - -test('preloading custom module that is not found should throw', async t => { - t.plan(2) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/Cannot find module/.test(err.message), err.message) - } - - const argv = ['-r', './test/data/require-missing.js', './examples/plugin.js'] - const fastify = await start.start(argv) - - await fastify.close() - t.pass('server closed') -}) - -test('preloading custom module should be done before starting server', async t => { - t.plan(4) - - const argv = ['./examples/plugin-with-preloaded.js'] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { hasPreloaded: true }) - - await fastify.close() - t.pass('server closed') -}) - -test('should support custom logger configuration', async t => { - t.plan(2) - - const argv = ['-L', './test/data/custom-logger.js', './examples/plugin.js'] - const fastify = await start.start(argv) - t.ok(fastify.log.test) - - await fastify.close() - t.pass('server closed') -}) - -test('preloading a built-in module works', async t => { - t.plan(1) - - const argv = ['-r', 'path', './examples/plugin.js'] - const fastify = await start.start(argv) - await fastify.close() - t.pass('server closed') -}) - -test('preloading a module in node_modules works', async t => { - t.plan(1) - - const argv = ['-r', 'tap', './examples/plugin.js'] - const fastify = await start.start(argv) - await fastify.close() - t.pass('server closed') -}) - -test('should throw on logger configuration module not found', async t => { - t.plan(2) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/Cannot find module/.test(err.message), err.message) - } - - const argv = ['-L', './test/data/missing.js', './examples/plugin.js'] - const fastify = await start.start(argv) - - await fastify.close() - t.pass('server closed') -}) - -test('should throw on async plugin with one argument', async t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/Async\/Await plugin function should contain 2 arguments./.test(err.message), err.message) - } - - const argv = ['./test/data/async-plugin-with-one-argument.js'] - await start.start(argv) -}) - -test('should start fastify with custom plugin options with a ESM typescript compiled plugin', { skip: !moduleSupport }, async t => { - t.plan(4) - - const argv = [ - '-p', - getPort(), - './examples/ts-plugin-with-custom-options.mjs', - '--', - '-abc', - '--hello', - 'world' - ] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { - a: true, - b: true, - c: true, - hello: 'world' - }) - - await fastify.close() - t.pass('server closed') - t.end() -}) - -test('should throw an error when loading ESM typescript compiled plugin and ESM is not supported', { skip: moduleSupport }, async t => { - t.plan(1) - - const oldStop = start.stop - t.teardown(() => { start.stop = oldStop }) - start.stop = function (err) { - t.ok(/Your version of node does not support ES modules./.test(err.message), err.message) - } - - const argv = ['./examples/ts-plugin-with-custom-options.mjs'] - await start.start(argv) - t.end() -}) - -test('should start fastify with custom plugin options with a ESM plugin with package.json "type":"module"', { skip: !moduleSupport }, async t => { - t.plan(4) - - const argv = [ - '-p', - getPort(), - './examples/package-type-module/ESM-plugin-with-custom-options.js', - '--', - '-abc', - '--hello', - 'world' - ] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { - a: true, - b: true, - c: true, - hello: 'world' - }) - - await fastify.close() - t.pass('server closed') - t.end() -}) - -test('should start fastify with custom plugin options with a CJS plugin with package.json "type":"module"', { skip: !moduleSupport }, async t => { - t.plan(4) - - const argv = [ - '-p', - getPort(), - './examples/package-type-module/CJS-plugin-with-custom-options.cjs', - '--', - '-abc', - '--hello', - 'world' - ] - const fastify = await start.start(argv) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${fastify.server.address().port}` - }) - - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { - a: true, - b: true, - c: true, - hello: 'world' - }) - - await fastify.close() - t.pass('server closed') - t.end() -}) diff --git a/test/templates/app-ts.test.ts b/test/templates/app-ts.test.ts deleted file mode 100644 index 3e396ad3..00000000 --- a/test/templates/app-ts.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { fastify } from 'fastify' -import { test } from 'tap' -const sgetOriginal = require('simple-get').concat - -import appDefault, { app } from '../../templates/app-ts/src/app' -import {AddressInfo} from "net"; - -const sget = (opts: Record): Record => { - return new Promise((resolve, reject) => { - sgetOriginal(opts, (err: Error, response: any, body: any) => { - if (err) return reject(err) - return resolve({ response, body }) - }) - }) -} - -test('should print routes for TS app', async t => { - t.plan(4) - - const fastifyApp = fastify({}, ); - await app(fastifyApp, {}); - await fastifyApp.ready(); - await fastifyApp.listen({ port: 3000 }) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${(fastifyApp.server.address() as AddressInfo).port}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { root: true }) - - await fastifyApp.close(); - t.pass('server closed') -}) - -test('should print routes for default TS app', async t => { - t.plan(4) - - const fastifyApp = fastify({}, ); - await appDefault(fastifyApp, {}); - await fastifyApp.ready(); - await fastifyApp.listen({ port: 3000 }) - - const { response, body } = await sget({ - method: 'GET', - url: `http://localhost:${(fastifyApp.server.address() as AddressInfo).port}` - }) - t.equal(response.statusCode, 200) - t.equal(response.headers['content-length'], '' + body.length) - t.same(JSON.parse(body), { root: true }) - - await fastifyApp.close(); - t.pass('server closed') -}) diff --git a/test/watch.test.js b/test/watch.test.js deleted file mode 100644 index 3b8bfabd..00000000 --- a/test/watch.test.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -const { arrayToRegExp } = require('../lib/watch/utils') - -const t = require('tap') -const test = t.test - -test('should equal expect RegExp', t => { - t.plan(1) - - const expectRegExp = /(node_modules|build|dist|\.git|bower_components|logs)/ - const regExp = arrayToRegExp(['node_modules', 'build', 'dist', '.git', 'bower_components', 'logs']) - - t.same(regExp, expectRegExp) -}) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..b36693e3 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "importHelpers": true, + "moduleResolution": "Node", + "resolveJsonModule": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*", "test/**/*", "templates/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4ccf1bdb..e47f236e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,23 @@ { - "extends": "fastify-tsconfig", - "compilerOptions": { - "esModuleInterop": true, - "noEmit": true - }, - "include": [ - "routes/**/*.ts", - "plugins/**/*.ts", - "test/**/*.ts", - "app.ts" - ] -} + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "importHelpers": true, + "moduleResolution": "Node", + "resolveJsonModule": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "skipLibCheck": true, + "esModuleInterop": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/util.js b/util.js deleted file mode 100644 index 28d024d2..00000000 --- a/util.js +++ /dev/null @@ -1,101 +0,0 @@ -const fs = require('fs') -const path = require('path') -const url = require('url') -const semver = require('semver') -const pkgUp = require('pkg-up') -const resolveFrom = require('resolve-from') - -const moduleSupport = semver.satisfies(process.version, '>= 14 || >= 12.17.0 < 13.0.0') - -function exit (message) { - if (message instanceof Error) { - console.log(message) - return process.exit(1) - } else if (message) { - console.log(`Warn: ${message}`) - return process.exit(1) - } - - process.exit() -} - -function requireModule (moduleName) { - if (fs.existsSync(moduleName)) { - const moduleFilePath = path.resolve(moduleName) - const moduleFileExtension = path.extname(moduleName) - const modulePath = moduleFilePath.split(moduleFileExtension)[0] - return require(modulePath) - } else { - return require(moduleName) - } -} - -function requireFastifyForModule (modulePath) { - try { - const basedir = path.resolve(process.cwd(), modulePath) - const module = require(resolveFrom.silent(basedir, 'fastify') || 'fastify') - - return { module } - } catch (e) { - exit('unable to load fastify module') - } -} - -function isInvalidAsyncPlugin (plugin) { - return plugin.length !== 2 && plugin.constructor.name === 'AsyncFunction' -} - -async function getPackageType (cwd) { - const nearestPackage = await pkgUp({ cwd }) - if (nearestPackage) { - return require(nearestPackage).type - } -} - -function getScriptType (fname, packageType) { - const modulePattern = /\.mjs$/i - const commonjsPattern = /\.cjs$/i - return (modulePattern.test(fname) ? 'module' : commonjsPattern.test(fname) ? 'commonjs' : packageType) || 'commonjs' -} - -async function requireServerPluginFromPath (modulePath) { - const resolvedModulePath = path.resolve(process.cwd(), modulePath) - - if (!fs.existsSync(resolvedModulePath)) { - throw new Error(`${resolvedModulePath} doesn't exist within ${process.cwd()}`) - } - - const packageType = await getPackageType(resolvedModulePath) - const type = getScriptType(resolvedModulePath, packageType) - - let serverPlugin - if (type === 'module') { - if (moduleSupport) { - serverPlugin = (await import(url.pathToFileURL(resolvedModulePath).href)).default - } else { - throw new Error(`fastify-cli cannot import plugin at '${resolvedModulePath}'. Your version of node does not support ES modules. To fix this error upgrade to Node 14 or use CommonJS syntax.`) - } - } else { - serverPlugin = require(resolvedModulePath) - } - - if (isInvalidAsyncPlugin(serverPlugin)) { - throw new Error('Async/Await plugin function should contain 2 arguments. ' + - 'Refer to documentation for more information.') - } - - return serverPlugin -} - -function showHelpForCommand (commandName) { - const helpFilePath = path.join(__dirname, 'help', `${commandName}.txt`) - - try { - console.log(fs.readFileSync(helpFilePath, 'utf8')) - exit() - } catch (e) { - exit(`unable to get help for command "${commandName}"`) - } -} - -module.exports = { exit, requireModule, requireFastifyForModule, showHelpForCommand, requireServerPluginFromPath } From 86ea25c3e3351b573676958ae4429ab46021431b Mon Sep 17 00:00:00 2001 From: KaKa Date: Mon, 6 Jun 2022 18:12:30 +0800 Subject: [PATCH 5/6] feat: command generate plugin --- README.md | 26 +++ src/commands/generate/plugin.ts | 224 ++++++++++++++++++++++ src/commands/generate/project.ts | 2 +- src/utils/package-json/base.ts | 24 +++ src/utils/package-json/plugin.ts | 103 ++++++++++ src/utils/package-json/project.ts | 169 ++++++++++++++++ src/utils/start.ts | 13 ++ src/utils/string.ts | 18 ++ templates/plugin/.github/dependabot.yml | 13 ++ templates/plugin/.github/workflows/ci.yml | 20 ++ templates/plugin/README.md.ejs | 30 +++ templates/plugin/__gitignore.ejs | 134 +++++++++++++ templates/plugin/index.d.ts.ejs | 13 ++ templates/plugin/index.js.ejs | 14 ++ templates/plugin/test/index.test-d.ts | 8 + templates/plugin/test/index.test.js | 14 ++ templates/plugin/tsconfig.json | 19 ++ 17 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 src/commands/generate/plugin.ts create mode 100644 src/utils/package-json/base.ts create mode 100644 src/utils/package-json/plugin.ts create mode 100644 src/utils/package-json/project.ts create mode 100644 src/utils/string.ts create mode 100644 templates/plugin/.github/dependabot.yml create mode 100644 templates/plugin/.github/workflows/ci.yml create mode 100644 templates/plugin/README.md.ejs create mode 100644 templates/plugin/__gitignore.ejs create mode 100644 templates/plugin/index.d.ts.ejs create mode 100644 templates/plugin/index.js.ejs create mode 100644 templates/plugin/test/index.test-d.ts create mode 100644 templates/plugin/test/index.test.js create mode 100644 templates/plugin/tsconfig.json diff --git a/README.md b/README.md index a97890a8..71bbffba 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,36 @@ USAGE # Commands +* [`fastify generate plugin [NAME]`](#fastify-generate-plugin-name) * [`fastify generate project [NAME]`](#fastify-generate-project-name) * [`fastify help [COMMAND]`](#fastify-help-command) * [`fastify start ENTRY`](#fastify-start-entry) +## `fastify generate plugin [NAME]` + +Generate fastify plugin + +``` +USAGE + $ fastify generate plugin [NAME] [--location ] [--overwrite] [--repo ] [--language ] [--lint + ] [--test ] [--help] + +ARGUMENTS + NAME Name of the plugin + +FLAGS + --help Show CLI help. + --language= Programming Language you would like to use in this project. + --lint= Lint Tools you would like to use in this project. + --location= Location to place the project. + --overwrite Force to overwrite the project location when it exist. + --repo= Git repository url of the project. + --test= Test Framework you would like to use in this project. + +DESCRIPTION + Generate fastify plugin +``` + ## `fastify generate project [NAME]` Generate fastify project diff --git a/src/commands/generate/plugin.ts b/src/commands/generate/plugin.ts new file mode 100644 index 00000000..23c9b6f8 --- /dev/null +++ b/src/commands/generate/plugin.ts @@ -0,0 +1,224 @@ +import { Flags } from '@oclif/core' +import { execSync } from 'child_process' +import { compile } from 'ejs' +import { access, mkdir, readFile, rm, stat, writeFile } from 'fs/promises' +import { prompt } from 'inquirer' +import { basename, dirname, join, resolve } from 'path' +import { Command } from '../../utils/command/command' +import { computePackageJSON } from '../../utils/package-json/plugin' +import { toCamelCase } from '../../utils/string' + +export default class Plugin extends Command { + static description = 'Generate fastify plugin' + + static args = [ + { name: 'name', required: false, description: 'Name of the plugin' } + ] + + static flags = { + location: Flags.string({ description: 'Location to place the project.' }), + overwrite: Flags.boolean({ description: 'Force to overwrite the project location when it exist.', default: false }), + repo: Flags.string({ description: 'Git repository url of the project.' }), + language: Flags.string({ description: 'Programming Language you would like to use in this project.' }), + lint: Flags.string({ description: 'Lint Tools you would like to use in this project.' }), + test: Flags.string({ description: 'Test Framework you would like to use in this project.' }), + help: Flags.help() + } + + shouldOverwrite = false + + async run (): Promise { + const { args, flags } = await this.parse(Plugin) + + // validate + if (flags.language !== undefined) this.corceLanguage(flags.language) + + const answer: any = {} + + Object.assign(answer, await prompt([ + { type: 'input', name: 'name', message: 'What is your project name?', validate: this.questionNameValidate }, + { type: 'input', name: 'location', message: 'Where do you want to place your project?', default: this.questionLocationDefault }, + { type: 'confirm', name: 'overwrite', message: 'The folder already exist. Do you want to overwrite?', default: flags.overwrite ?? false, when: this.questionOverwriteWhen, askAnswered: true } + ], { + name: args.name, + location: flags.location, + overwrite: flags.overwrite + })) + + if (this.shouldOverwrite) this.questionOverwriteValidate(answer.overwrite) + + Object.assign(answer, await prompt([ + { type: 'input', name: 'repo', message: 'What is the repo url?' }, + { type: 'list', name: 'language', message: 'Which language will you use?', default: 'JavaScript', choices: ['JavaScript'] }, + { type: 'list', name: 'lint', message: 'Which linter would you like to use?', default: this.questionLintDefault, choices: this.questionLintChoices }, + { type: 'list', name: 'test', message: 'Which test framework would you like to use?', default: 'tap', choices: ['tap'] } + ], { + repo: flags.repo, + language: flags.language, + lint: flags.lint, + test: flags.test + })) + + answer.camelCaseName = toCamelCase(answer.name) + + const fileList = await this.computeFileList(answer) + const files = await this.prepareFiles(fileList, answer) + await this.writeFiles(files, answer) + await this.npmInstall(answer) + this.log(`project "${answer.name as string}" initialized in "${answer.location as string}"`) + } + + questionNameValidate = (input: string): true | string => { + if (String(input).trim() === '') { + return 'Project Name cannot be empty.' + } + return true + } + + questionLocationDefault = (answer: any): string => { + return this.toLocation(answer.name) + } + + questionOverwriteWhen = async (answer: any): Promise => { + this.shouldOverwrite = !(await this.validateProjectLocation(answer.location)) + return this.shouldOverwrite + } + + questionOverwriteValidate = (input: boolean): true | undefined => { + if (input) return true + this.log('Terminated because location cannot be overwrite.') + this.exit(0) + } + + questionLintDefault =(answer: any): string => { + switch (this.corceLanguage(answer.language)) { + case 'JavaScript': + return 'standard' + } + } + + questionLintChoices = (answer: any): string[] => { + switch (this.corceLanguage(answer.language)) { + case 'JavaScript': + return ['standard', 'eslint', 'eslint + standard'] + } + } + + corceLanguage (str: string): 'JavaScript' { + switch (str.trim().toLowerCase()) { + case 'js': + case 'javascript': + return 'JavaScript' + default: + throw new Error(`Programming Language expected to be "JavaScript", but recieved "${str}"`) + } + } + + toLocation (name: string): string { + return name.trim().toLowerCase().replace(/\s+/g, '-') + } + + async isFileExist (path: string): Promise { + try { + const stats = await stat(path) + return stats.isFile() + } catch { + return false + } + } + + async validateProjectLocation (location: string): Promise { + const path = resolve(location) + try { + await access(path) + return false + } catch { + return true + } + } + + computeFileList (answer: any): string[] { + // we do not add .ejs in here + // we should find the file if .ejs exist first and then compile to the destination + const files: string[] = [ + 'README.md', + '__gitignore', + '.github/dependabot.yml', + '.github/workflows/ci.yml' + ] + + if (answer.language === 'JavaScript') { + files.push('tsconfig.json') + files.push('index.js') + files.push('index.d.ts') + files.push('test/index.test.js') + files.push('test/index.test-d.ts') + } + + return files + } + + async resolveFile (file: string): Promise<{ template: boolean, content: string }> { + const o = { template: false, content: '' } + const ejsPath = resolve(join('templates', 'plugin', `${file}.ejs`)) + const isEJSTemplate = await this.isFileExist(ejsPath) + if (isEJSTemplate) { + o.template = true + const data = await readFile(ejsPath) + o.content = data.toString() + return o + } + const filePath = resolve(join('templates', 'plugin', file)) + const isFileExist = await this.isFileExist(filePath) + if (isFileExist) { + const data = await readFile(filePath) + o.content = data.toString() + return o + } + throw new Error(`File ${file} is missing, please check if the module installed properly.`) + } + + async prepareFiles (files: string[], answer: any): Promise<{ [path: string]: string }> { + const o: { [path: string]: string } = {} + o['package.json'] = computePackageJSON(answer) + for (let file of files) { + const { template, content } = await this.resolveFile(file) + const dir = dirname(file) + file = `${dir}/${basename(file).replace('__', '.')}` + if (template) { + const render = compile(content, { async: true }) + o[file] = await render(answer) + } else { + o[file] = content + } + } + return o + } + + async writeFiles (files: { [path: string]: string }, answer: any): Promise { + if (this.shouldOverwrite) { + this.log(`remove folder "${answer.location as string}"`) + await rm(resolve(answer.location as string), { recursive: true, force: true }) + } + for (const [path, content] of Object.entries(files)) { + const realpath = join(answer.location, path) + const fullpath = resolve(realpath) + await mkdir(dirname(fullpath), { recursive: true }) + await writeFile(fullpath, content) + this.log(`write file "${path}" to "${realpath}"`) + } + } + + async npmInstall (answer: any): Promise { + this.log('run "npm install"') + const result: any = await prompt([ + { type: 'confirm', name: 'npm', message: 'Do you want to run "npm install"?', default: true } + ]) + if (result.npm === true) { + execSync('npm install', { + cwd: resolve(answer.location), + stdio: 'inherit' + }) + } + } +} diff --git a/src/commands/generate/project.ts b/src/commands/generate/project.ts index cc0d40fb..dc504b6a 100644 --- a/src/commands/generate/project.ts +++ b/src/commands/generate/project.ts @@ -5,7 +5,7 @@ import { access, mkdir, readFile, rm, stat, writeFile } from 'fs/promises' import { prompt } from 'inquirer' import { basename, dirname, join, resolve } from 'path' import { Command } from '../../utils/command/command' -import { computePackageJSON } from '../../utils/package-json' +import { computePackageJSON } from '../../utils/package-json/project' export default class Project extends Command { static description = 'Generate fastify project' diff --git a/src/utils/package-json/base.ts b/src/utils/package-json/base.ts new file mode 100644 index 00000000..e58b503c --- /dev/null +++ b/src/utils/package-json/base.ts @@ -0,0 +1,24 @@ +import { resolve } from 'path' + +export function findPackageJSON (): { + version: string + dependencies: { [key: string]: string } + devDependencies: { [key: string]: string } +} { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require(resolve('package.json')) + return { + version: pkg.version, + dependencies: pkg.dependencies, + devDependencies: pkg.devDependencies + } +} + +export function sort (dependencies: { [key: string]: string }): { [key: string]: string } { + const keys = Object.keys(dependencies).sort() + const obj: { [key: string]: string } = {} + for (const key of keys) { + obj[key] = dependencies[key] + } + return obj +} diff --git a/src/utils/package-json/plugin.ts b/src/utils/package-json/plugin.ts new file mode 100644 index 00000000..e7dd0295 --- /dev/null +++ b/src/utils/package-json/plugin.ts @@ -0,0 +1,103 @@ +import { findPackageJSON, sort } from './base' + +export function computePackageJSON (answer: any): string { + const pkg = findPackageJSON() + const template: any = {} + template.name = answer.name + template.main = 'index.js' + template.scripts = computeScripts(answer) + template.dependencies = computeDependencies(answer, pkg) + template.devDependencies = computeDevDependencies(answer, pkg) + if (String(answer.lint).includes('eslint')) { + template.eslintConfig = computeESLintConfig(template.devDependencies) + } + template.tsd = computeTSDConfig() + + return JSON.stringify(template, null, 2) +} + +export function computeScripts (answer: any): { [key: string]: string } { + const scripts: any = {} + // Lint Command + if (answer.lint === 'standard') { + scripts.lint = 'standard' + } + if (String(answer.lint).includes('eslint')) { + scripts.lint = 'eslint .' + } + + // Other Commands + if (answer.language === 'JavaScript') { + scripts.test = 'npm run unit && npm run tsd' + scripts.unit = 'tap "test/**/*.test.js"' + scripts.tsd = 'tsd' + } + return scripts +} + +export function computeDependencies (_answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies['fastify-plugin'] = pkg.dependencies['fastify-plugin'] + return sort(dependencies) +} + +export function computeDevDependencies (answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies.tap = pkg.devDependencies.tap + dependencies.tsd = pkg.devDependencies.tsd + dependencies.fastify = pkg.dependencies.fastify + + if (String(answer.lint).includes('eslint')) { + // shared + dependencies.eslint = '^8.16.0' + dependencies['eslint-plugin-import'] = pkg.devDependencies['eslint-plugin-import'] + dependencies['eslint-plugin-promise'] = '^6.0.0' + + // standard + if (String(answer.lint).includes('standard')) { + dependencies['eslint-config-standard'] = '^17.0.0' + dependencies['eslint-plugin-n'] = '^15.2.0' + } + } + if (answer.lint === 'standard') { + dependencies.standard = '^17.0.0' + } + + return sort(dependencies) +} + +export function computeESLintConfig (devDependencies: { [key: string]: string }): any { + const keys = Object.keys(devDependencies) + // standard + if (keys.includes('eslint-config-standard')) { + return { + extends: 'standard' + } + } + // eslint + const config: { + extends: string[] + plugins: string[] + [key: string]: any + } = { + extends: ['eslint:recommended'], + plugins: [] + } + if (keys.includes('eslint-plugin-promise')) { + config.plugins.push('promise') + } + if (keys.includes('eslint-plugin-import')) { + config.extends.push('plugin:import/recommended') + config.plugins.push('import') + } + if (keys.includes('eslint-plugin-n')) { + config.extends.push('plugin:n/recommended') + } + return config +} + +export function computeTSDConfig (): any { + return { + directory: 'test' + } +} diff --git a/src/utils/package-json/project.ts b/src/utils/package-json/project.ts new file mode 100644 index 00000000..d3827084 --- /dev/null +++ b/src/utils/package-json/project.ts @@ -0,0 +1,169 @@ +import { findPackageJSON, sort } from './base' + +export function computePackageJSON (answer: any): string { + const pkg = findPackageJSON() + const template: any = {} + template.name = answer.name + template.main = answer.language === 'JavaScript' ? 'app.js' : 'dist/app.js' + template.scripts = computeScripts(answer) + template.dependencies = computeDependencies(answer, pkg) + template.devDependencies = computeDevDependencies(answer, pkg) + if (String(answer.lint).includes('eslint')) { + template.eslintConfig = computeESLintConfig(template.devDependencies) + } + + return JSON.stringify(template, null, 2) +} + +export function computeScripts (answer: any): { [key: string]: string } { + const scripts: any = {} + // Lint Command + if (answer.lint === 'standard') { + scripts.lint = 'standard' + } + if (answer.lint === 'ts-standard') { + scripts.lint = 'ts-standard' + } + if (String(answer.lint).includes('eslint')) { + scripts.lint = `eslint . ${answer.language === 'TypeScript' ? '--ext .ts' : ''}` + } + + // Other Commands + if (answer.language === 'JavaScript') { + scripts.test = 'tap "test/**/*.test.js"' + scripts.start = 'fastify start -l info app.js' + scripts.dev = 'fastify start -w -l info -P app.js' + } + if (answer.language === 'TypeScript') { + scripts.test = 'tap "test/**/*.test.ts" --ts' + scripts.start = 'fastify start -r ts-node/register -l info app.js' + scripts.dev = 'fastify start -r ts-node/register -w -l info -P app.js' + } + return scripts +} + +export function computeDependencies (_answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies['@fastify/autoload'] = pkg.dependencies['@fastify/autoload'] + dependencies['@fastify/sensible'] = pkg.dependencies['@fastify/sensible'] + dependencies.fastify = pkg.dependencies.fastify + dependencies['fastify-cli'] = pkg.version + dependencies['fastify-plugin'] = pkg.dependencies['fastify-plugin'] + return sort(dependencies) +} + +// ts-standard + +export function computeDevDependencies (answer: any, pkg: any): { [key: string]: string } { + const dependencies: any = {} + dependencies.tap = pkg.devDependencies.tap + if (answer.language === 'TypeScript') { + dependencies['@types/tap'] = pkg.devDependencies['@types/tap'] + dependencies['@types/node'] = pkg.devDependencies['@types/node'] + dependencies['ts-node'] = pkg.devDependencies['ts-node'] + dependencies.typescript = pkg.devDependencies.typescript + } + if (String(answer.lint).includes('eslint')) { + // shared + dependencies.eslint = '^8.16.0' + dependencies['eslint-plugin-import'] = pkg.devDependencies['eslint-plugin-import'] + dependencies['eslint-plugin-promise'] = pkg.devDependencies['eslint-plugin-promise'] + + // ts-standard + if (String(answer.lint).includes('ts-standard')) { + dependencies.eslint = pkg.devDependencies.eslint + dependencies['eslint-plugin-node'] = pkg.devDependencies['eslint-plugin-node'] + dependencies['eslint-config-standard-with-typescript'] = pkg.devDependencies['eslint-config-standard-with-typescript'] + // require >=3.3.1 <4.5.0 + dependencies.typescript = '~4.4.0' + } else + // standard + if (String(answer.lint).includes('standard')) { + dependencies['eslint-config-standard'] = '^17.0.0' + } + + // non ts-standard + if (!String(answer.lint).includes('ts-standard')) { + // eslint, eslint + standard + // override version + dependencies['eslint-plugin-promise'] = '^6.0.0' + // eslint, eslint + standard + dependencies['eslint-plugin-n'] = '^15.2.0' + } + + // TypeScript + if (answer.language === 'TypeScript') { + dependencies['@typescript-eslint/eslint-plugin'] = pkg.devDependencies['@typescript-eslint/eslint-plugin'] + if (!String(answer.lint).includes('ts-standard')) { + dependencies['@typescript-eslint/parser'] = '^5.25.0' + } + } + } + if (answer.lint === 'standard') { + dependencies.standard = '^17.0.0' + } + if (answer.lint === 'ts-standard') { + dependencies['ts-standard'] = '^11.0.0' + } + + return sort(dependencies) +} + +export function computeESLintConfig (devDependencies: { [key: string]: string }): any { + const keys = Object.keys(devDependencies) + // ts-standard + if (keys.includes('eslint-config-standard-with-typescript')) { + return { + extends: 'standard-with-typescript', + parserOptions: { + project: './tsconfig.json' + }, + overrides: [ + { + files: [ + 'test/**/*.test.ts' + ], + rules: { + '@typescript-eslint/no-floating-promises': 'off' + } + } + ] + } + } + // standard + if (keys.includes('eslint-config-standard')) { + return { + extends: 'standard' + } + } + // eslint + const config: { + extends: string[] + plugins: string[] + [key: string]: any + } = { + extends: ['eslint:recommended'], + plugins: [] + } + if (keys.includes('eslint-plugin-promise')) { + config.plugins.push('promise') + } + if (keys.includes('eslint-plugin-import')) { + config.extends.push('plugin:import/recommended') + config.plugins.push('import') + } + if (keys.includes('eslint-plugin-n')) { + config.extends.push('plugin:n/recommended') + } + if (keys.includes('eslint-plugin-node')) { + config.extends.push('plugin:node/recommended') + } + if (keys.includes('@typescript-eslint/eslint-plugin')) { + config.plugins.push('@typescript-eslint') + config.extends.push('plugin:@typescript-eslint/recommended') + } + if (keys.includes('@typescript-eslint/parser')) { + config.parser = '@typescript-eslint/parser' + } + return config +} diff --git a/src/utils/start.ts b/src/utils/start.ts index c19593f1..7d4f933d 100644 --- a/src/utils/start.ts +++ b/src/utils/start.ts @@ -1,3 +1,4 @@ +import closeWithGrace from 'close-with-grace' import { FastifyInstance } from 'fastify' import { constants } from 'fs' import { access } from 'fs/promises' @@ -94,6 +95,18 @@ export async function start (_o?: Partial): Promise /[À-ž]/.test(m) ? m : '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, m => '-') + .toLowerCase() +} diff --git a/templates/plugin/.github/dependabot.yml b/templates/plugin/.github/dependabot.yml new file mode 100644 index 00000000..618a234e --- /dev/null +++ b/templates/plugin/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/templates/plugin/.github/workflows/ci.yml b/templates/plugin/.github/workflows/ci.yml new file mode 100644 index 00000000..3955563d --- /dev/null +++ b/templates/plugin/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node-version: [14, 16, 18] + os: [macos-latest, ubuntu-latest, windows-latest] + steps: + - name: Check out repo + uses: actions/checkout@v3 + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install --ignore-scripts + - name: Run tests + run: npm run test \ No newline at end of file diff --git a/templates/plugin/README.md.ejs b/templates/plugin/README.md.ejs new file mode 100644 index 00000000..b522b18a --- /dev/null +++ b/templates/plugin/README.md.ejs @@ -0,0 +1,30 @@ +# <%= name %> + +![CI](<%= repo %>/workflows/CI/badge.svg) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) + +Supports Fastify versions `4.x` + +## Install +``` +npm i <%= name %> +``` + +## Usage +Require `<%= name %>` and register. +```js +const fastify = require('fastify')() +const <%= camelCaseName %> = require('<%= name %>') + +fastify.register(<%= camelCaseName %>, { + // put your options here +}) + +fastify.listen({ port: 3000 }) +``` + +## Acknowledgements + +## License + +Licensed under [MIT](./LICENSE).
\ No newline at end of file diff --git a/templates/plugin/__gitignore.ejs b/templates/plugin/__gitignore.ejs new file mode 100644 index 00000000..d071b972 --- /dev/null +++ b/templates/plugin/__gitignore.ejs @@ -0,0 +1,134 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# lock files +package-lock.json +yarn.lock + +# editor files +.vscode +.idea + +<% if(language === "TypeScript") { %> +**/*.js +<% } %> \ No newline at end of file diff --git a/templates/plugin/index.d.ts.ejs b/templates/plugin/index.d.ts.ejs new file mode 100644 index 00000000..877110f7 --- /dev/null +++ b/templates/plugin/index.d.ts.ejs @@ -0,0 +1,13 @@ +import { FastifyPluginAsync } from 'fastify' + +declare module 'fastify' { + export interface FastifyInstance { + // This is an example decorator type added to fastify + exampleDecorator: () => string + } +} + +declare const <%= camelCaseName %>: FastifyPluginAsync<() => string> + +export { <%= camelCaseName %> } +export default <%= camelCaseName %> \ No newline at end of file diff --git a/templates/plugin/index.js.ejs b/templates/plugin/index.js.ejs new file mode 100644 index 00000000..896b5c3a --- /dev/null +++ b/templates/plugin/index.js.ejs @@ -0,0 +1,14 @@ +'use strict' + +const fp = require('fastify-plugin') + +async function <%= camelCaseName %> (fastify, opts) { + fastify.decorate('exampleDecorator', function () { + return 'decorated' + }) +} + +module.exports = fp(<%= camelCaseName %>, { + name: '<%= name %>', + fastify: '4.x' +}) diff --git a/templates/plugin/test/index.test-d.ts b/templates/plugin/test/index.test-d.ts new file mode 100644 index 00000000..c47aa722 --- /dev/null +++ b/templates/plugin/test/index.test-d.ts @@ -0,0 +1,8 @@ +import Fastify from 'fastify' +import { expectType } from 'tsd' +import plugin from '..' + +const fastify = Fastify() +void fastify.register(plugin) + +expectType<() => string>(fastify.exampleDecorator) diff --git a/templates/plugin/test/index.test.js b/templates/plugin/test/index.test.js new file mode 100644 index 00000000..40e902a8 --- /dev/null +++ b/templates/plugin/test/index.test.js @@ -0,0 +1,14 @@ +const { test } = require('tap') +const Fastify = require('fastify') + +test('should register the correct decorator', async function (t) { + t.plan(1) + + const fastify = Fastify() + + fastify.register(require('..')) + + await fastify.ready() + + t.same(fastify.exampleDecorator(), 'decorated') +}) diff --git a/templates/plugin/tsconfig.json b/templates/plugin/tsconfig.json new file mode 100644 index 00000000..f2b1b202 --- /dev/null +++ b/templates/plugin/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "moduleResolution": "Node", + "resolveJsonModule": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "skipLibCheck": true, + "esModuleInterop": true, + }, + "exclude": ["node_modules"] +} \ No newline at end of file From 56c9bb1fe6fcfe21c51f25df2a603143b1888102 Mon Sep 17 00:00:00 2001 From: KaKa Date: Mon, 6 Jun 2022 18:12:52 +0800 Subject: [PATCH 6/6] chore: use colorette and close-with-grace --- package.json | 6 ++++-- src/utils/watch/fastify.ts | 10 +++++----- src/utils/watch/watch.ts | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 2cac36ee..0730ef36 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prepack": "npm run build", "pack": "oclif manifest && oclif readme", "postpack": "shx rm -f oclif.manifest.json", - "preunit": "npm run pack", + "preunit": "npm run build", "unit": "tap \"test/**/*.test.ts\"", "test": "npm run lint && npm run unit", "snap": "TAP_SNAPSHOT=1 && npm run unit" @@ -53,8 +53,9 @@ "@fastify/sensible": "^5.0.0", "@oclif/core": "^1.8.2", "@oclif/plugin-help": "^5.1.12", - "ansi-colors": "^4.1.3", "chokidar": "^3.5.3", + "close-with-grace": "^1.1.0", + "colorette": "^2.0.17", "ejs": "^3.1.8", "fastify": "^4.0.0-rc.2", "fastify-plugin": "^3.0.0", @@ -82,6 +83,7 @@ "shx": "^0.3.4", "tap": "^16.2.0", "ts-node": "^10.7.0", + "tsd": "^0.20.0", "typescript": "~4.4.0" }, "oclif": { diff --git a/src/utils/watch/fastify.ts b/src/utils/watch/fastify.ts index b21d5f3c..c0d97328 100644 --- a/src/utils/watch/fastify.ts +++ b/src/utils/watch/fastify.ts @@ -1,11 +1,11 @@ -import C from 'ansi-colors' +import { red } from 'colorette' import { FastifyInstance } from 'fastify' import { start, stop } from '../start' import { TIMEOUT } from './constants' import { GRACEFUL_SHUTDOWN, READY } from './events' function exit (this: any): void { - if (this as boolean) { console.log(C.red('[fastify-cli] process forced end')) } + if (this as boolean) { console.log(red('[fastify-cli] process forced end')) } process.exit(1) } @@ -15,7 +15,7 @@ let fastify: null | FastifyInstance = null process.on('message', function (event) { console.log('message', event) if (event === GRACEFUL_SHUTDOWN) { - const message = C.red('[fastify-cli] process forced end') + const message = red('[fastify-cli] process forced end') setTimeout(exit.bind({ message }), TIMEOUT).unref() if (fastify !== null) { fastify.close(function () { @@ -28,8 +28,8 @@ process.on('message', function (event) { }) process.on('uncaughtException', function (err) { - console.log(C.red(err as never as string)) - const message = C.red('[fastify-cli] app crashed - waiting for file changes before starting...') + console.log(red(err as never as string)) + const message = red('[fastify-cli] app crashed - waiting for file changes before starting...') exit.bind({ message })() }) diff --git a/src/utils/watch/watch.ts b/src/utils/watch/watch.ts index 429bfb36..46608b3b 100644 --- a/src/utils/watch/watch.ts +++ b/src/utils/watch/watch.ts @@ -1,5 +1,5 @@ -import C from 'ansi-colors' import { ChildProcess, fork } from 'child_process' +import { gray, red } from 'colorette' import EventEmitter from 'events' import { join, relative } from 'path' import { normalizeStartOptions, StartOption } from '../start' @@ -25,7 +25,7 @@ export async function watch (_o?: Partial): Promise): Promise): Promise