From 5299eb1f47088a27673ee2594d8e560827028578 Mon Sep 17 00:00:00 2001 From: S-Abhishek Date: Thu, 26 Jan 2023 14:22:58 +0530 Subject: [PATCH] feat: add support for typescript and esm generation (#577) This commit will be amended with better implementation soon. --- .taprc | 1 + generate.js | 14 +- package.json | 5 +- templates/app-ts-esm/README.md | 23 +++ templates/app-ts-esm/__gitignore | 65 ++++++ templates/app-ts-esm/__taprc | 4 + templates/app-ts-esm/package.json | 3 + templates/app-ts-esm/src/app.ts | 46 +++++ templates/app-ts-esm/src/plugins/README.md | 16 ++ templates/app-ts-esm/src/plugins/sensible.ts | 11 + templates/app-ts-esm/src/plugins/support.ts | 20 ++ templates/app-ts-esm/src/routes/README.md | 24 +++ .../app-ts-esm/src/routes/example/index.ts | 9 + templates/app-ts-esm/src/routes/root.ts | 9 + templates/app-ts-esm/test/helper.ts | 40 ++++ .../app-ts-esm/test/plugins/support.test.ts | 11 + .../app-ts-esm/test/routes/example.test.ts | 12 ++ templates/app-ts-esm/test/routes/root.test.ts | 11 + templates/app-ts-esm/test/tsconfig.json | 8 + templates/app-ts-esm/tsconfig.json | 11 + test/generate-typescript-esm.test.js | 192 ++++++++++++++++++ 21 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 templates/app-ts-esm/README.md create mode 100644 templates/app-ts-esm/__gitignore create mode 100644 templates/app-ts-esm/__taprc create mode 100644 templates/app-ts-esm/package.json create mode 100644 templates/app-ts-esm/src/app.ts create mode 100644 templates/app-ts-esm/src/plugins/README.md create mode 100644 templates/app-ts-esm/src/plugins/sensible.ts create mode 100644 templates/app-ts-esm/src/plugins/support.ts create mode 100644 templates/app-ts-esm/src/routes/README.md create mode 100644 templates/app-ts-esm/src/routes/example/index.ts create mode 100644 templates/app-ts-esm/src/routes/root.ts create mode 100644 templates/app-ts-esm/test/helper.ts create mode 100644 templates/app-ts-esm/test/plugins/support.test.ts create mode 100644 templates/app-ts-esm/test/routes/example.test.ts create mode 100644 templates/app-ts-esm/test/routes/root.test.ts create mode 100644 templates/app-ts-esm/test/tsconfig.json create mode 100644 templates/app-ts-esm/tsconfig.json create mode 100644 test/generate-typescript-esm.test.js diff --git a/.taprc b/.taprc index 85101415..02afe24b 100644 --- a/.taprc +++ b/.taprc @@ -4,3 +4,4 @@ jobs: 1 reporter: terse check-coverage: false timeout: 60 +test-ignore: templates/app-ts-esm/.* \ No newline at end of file diff --git a/generate.js b/generate.js index 54806ec6..4b6ba6a9 100755 --- a/generate.js +++ b/generate.js @@ -159,7 +159,19 @@ function cli (args) { let template if (opts.lang === 'ts' || opts.lang === 'typescript') { - template = typescriptTemplate + template = { ...typescriptTemplate } + + if (opts.esm) { + template.dir = 'app-ts-esm' + template.type = 'module' + template.tap = { + 'node-arg': [ + '--no-warnings', + '--experimental-loader', + 'ts-node/esm' + ] + } + } } else { template = { ...javascriptTemplate } diff --git a/package.json b/package.json index 62cd5dd0..3865837d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "scripts": { "lint": "standard", "lint:fix": "standard --fix", - "unit": "tap \"test/**/*.test.{js,ts}\" \"templates/**/*.test.{js,ts}\" --timeout 300", + "unit-ts-esm": "TS_NODE_PROJECT=./templates/app-ts-esm/tsconfig.json tap --node-arg=--loader=ts-node/esm templates/app-ts-esm/test/**/*.test.ts", + "unit": "tap --test-regex='test/.*\\.test\\.(js|ts)' --timeout 300", "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 unit && npm run test:typescript", + "test": "npm run unit && npm run unit-ts-esm && npm run test:typescript", "test:typescript": "tsd templates/plugin && tsc --project templates/app-ts/tsconfig.json && del-cli templates/app-ts/dist" }, "keywords": [ diff --git a/templates/app-ts-esm/README.md b/templates/app-ts-esm/README.md new file mode 100644 index 00000000..be35d934 --- /dev/null +++ b/templates/app-ts-esm/README.md @@ -0,0 +1,23 @@ +# 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-esm/__gitignore b/templates/app-ts-esm/__gitignore new file mode 100644 index 00000000..f4cefe89 --- /dev/null +++ b/templates/app-ts-esm/__gitignore @@ -0,0 +1,65 @@ +# 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-esm/__taprc b/templates/app-ts-esm/__taprc new file mode 100644 index 00000000..d6fd5343 --- /dev/null +++ b/templates/app-ts-esm/__taprc @@ -0,0 +1,4 @@ +test-env: [ + TS_NODE_FILES=true, + TS_NODE_PROJECT=./test/tsconfig.json +] diff --git a/templates/app-ts-esm/package.json b/templates/app-ts-esm/package.json new file mode 100644 index 00000000..47200257 --- /dev/null +++ b/templates/app-ts-esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/templates/app-ts-esm/src/app.ts b/templates/app-ts-esm/src/app.ts new file mode 100644 index 00000000..1aa38221 --- /dev/null +++ b/templates/app-ts-esm/src/app.ts @@ -0,0 +1,46 @@ +import * as path from 'path'; +import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload'; +import { FastifyPluginAsync } from 'fastify'; +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export type AppOptions = { + // Place your custom options for app below here. +} & Partial; + + +// Pass --options via CLI arguments in command to enable these options. +const options: AppOptions = { +} + +const app: FastifyPluginAsync = async ( + fastify, + opts +): Promise => { + // Place here your custom code! + + // Do not touch the following lines + + // This loads all plugins defined in plugins + // those should be support plugins that are reused + // through your application + void fastify.register(AutoLoad, { + dir: path.join(__dirname, 'plugins'), + options: opts, + forceESM: true + }) + + // This loads all plugins defined in routes + // define your routes in one of these + void fastify.register(AutoLoad, { + dir: path.join(__dirname, 'routes'), + options: opts, + forceESM: true + }) + +}; + +export default app; +export { app, options } diff --git a/templates/app-ts-esm/src/plugins/README.md b/templates/app-ts-esm/src/plugins/README.md new file mode 100644 index 00000000..02fd5f93 --- /dev/null +++ b/templates/app-ts-esm/src/plugins/README.md @@ -0,0 +1,16 @@ +# Plugins Folder + +Plugins define behavior that is common to all the routes in your +application. Authentication, caching, templates, and all the other cross +cutting concerns should be handled by plugins placed in this folder. + +Files in this folder are typically defined through the +[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, +making them non-encapsulated. They can define decorators and set hooks +that will then be used in the rest of your application. + +Check out: + +* [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Guides/Plugins-Guide/) +* [Fastify decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). +* [Fastify lifecycle](https://www.fastify.io/docs/latest/Reference/Lifecycle/). diff --git a/templates/app-ts-esm/src/plugins/sensible.ts b/templates/app-ts-esm/src/plugins/sensible.ts new file mode 100644 index 00000000..b1230219 --- /dev/null +++ b/templates/app-ts-esm/src/plugins/sensible.ts @@ -0,0 +1,11 @@ +import fp from 'fastify-plugin' +import sensible, { SensibleOptions } from '@fastify/sensible' + +/** + * This plugins adds some utilities to handle http errors + * + * @see https://github.com/fastify/fastify-sensible + */ +export default fp(async (fastify) => { + fastify.register(sensible) +}) diff --git a/templates/app-ts-esm/src/plugins/support.ts b/templates/app-ts-esm/src/plugins/support.ts new file mode 100644 index 00000000..94bae4f5 --- /dev/null +++ b/templates/app-ts-esm/src/plugins/support.ts @@ -0,0 +1,20 @@ +import fp from 'fastify-plugin' + +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) => { + fastify.decorate('someSupport', function () { + return 'hugs' + }) +}) + +// When using .decorate you have to specify added properties for Typescript +declare module 'fastify' { + export interface FastifyInstance { + someSupport(): string; + } +} diff --git a/templates/app-ts-esm/src/routes/README.md b/templates/app-ts-esm/src/routes/README.md new file mode 100644 index 00000000..67b739aa --- /dev/null +++ b/templates/app-ts-esm/src/routes/README.md @@ -0,0 +1,24 @@ +# Routes Folder + +Routes define endpoints within your application. Fastify provides an +easy path to a microservice architecture, in the future you might want +to independently deploy some of those. + +In this folder you should define all the routes that define the endpoints +of your web application. +Each service is a [Fastify +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. + +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 +by the application. You can now add as many files as you want inside that folder. +In this way you can create complex routes within a single monolith, +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/). diff --git a/templates/app-ts-esm/src/routes/example/index.ts b/templates/app-ts-esm/src/routes/example/index.ts new file mode 100644 index 00000000..819c5e77 --- /dev/null +++ b/templates/app-ts-esm/src/routes/example/index.ts @@ -0,0 +1,9 @@ +import { FastifyPluginAsync } from "fastify" + +const example: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.get('/', async function (request, reply) { + return 'this is an example' + }) +} + +export default example; diff --git a/templates/app-ts-esm/src/routes/root.ts b/templates/app-ts-esm/src/routes/root.ts new file mode 100644 index 00000000..2a1b3342 --- /dev/null +++ b/templates/app-ts-esm/src/routes/root.ts @@ -0,0 +1,9 @@ +import { FastifyPluginAsync } from 'fastify' + +const root: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.get('/', async function (request, reply) { + return { root: true } + }) +} + +export default root; diff --git a/templates/app-ts-esm/test/helper.ts b/templates/app-ts-esm/test/helper.ts new file mode 100644 index 00000000..ccc705eb --- /dev/null +++ b/templates/app-ts-esm/test/helper.ts @@ -0,0 +1,40 @@ +// This file contains code that we reuse between our tests. +// @ts-ignore +import helper from 'fastify-cli/helper.js' +import * as path from 'path' +import * as tap from 'tap'; +import { fileURLToPath } from 'url' + + +export type Test = typeof tap['Test']['prototype']; + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const AppPath = path.join(__dirname, '..', 'src', 'app.ts') + +// Fill in this config with all the configurations +// needed for testing the application +async function config () { + return {} +} + +// Automatically build and tear down our instance +async function build (t: Test) { + // you can set all the options supported by the fastify CLI command + const argv = [AppPath] + + // fastify-plugin ensures that all decorators + // are exposed for testing purposes, this is + // different from the production setup + const app = await helper.build(argv, await config()) + + // Tear down our app after we are done + t.teardown(() => void app.close()) + + return app +} + +export { + config, + build +} diff --git a/templates/app-ts-esm/test/plugins/support.test.ts b/templates/app-ts-esm/test/plugins/support.test.ts new file mode 100644 index 00000000..63725f79 --- /dev/null +++ b/templates/app-ts-esm/test/plugins/support.test.ts @@ -0,0 +1,11 @@ +import { test } from 'tap' +import Fastify from 'fastify' +import Support from '../../src/plugins/support.js' + +test('support works standalone', async (t) => { + const fastify = Fastify() + void fastify.register(Support) + await fastify.ready() + + t.equal(fastify.someSupport(), 'hugs') +}) diff --git a/templates/app-ts-esm/test/routes/example.test.ts b/templates/app-ts-esm/test/routes/example.test.ts new file mode 100644 index 00000000..63279968 --- /dev/null +++ b/templates/app-ts-esm/test/routes/example.test.ts @@ -0,0 +1,12 @@ +import { test } from 'tap' +import { build } from '../helper.js' + +test('example is loaded', async (t) => { + const app = await build(t) + + const res = await app.inject({ + url: '/example' + }) + + t.equal(res.payload, 'this is an example') +}) diff --git a/templates/app-ts-esm/test/routes/root.test.ts b/templates/app-ts-esm/test/routes/root.test.ts new file mode 100644 index 00000000..a2378ac8 --- /dev/null +++ b/templates/app-ts-esm/test/routes/root.test.ts @@ -0,0 +1,11 @@ +import { test } from 'tap' +import { build } from '../helper.js' + +test('default root route', async (t) => { + const app = await build(t) + + const res = await app.inject({ + url: '/' + }) + t.same(JSON.parse(res.payload), { root: true }) +}) diff --git a/templates/app-ts-esm/test/tsconfig.json b/templates/app-ts-esm/test/tsconfig.json new file mode 100644 index 00000000..3b443e0d --- /dev/null +++ b/templates/app-ts-esm/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": false + }, + "include": ["../src/**/*.ts", "**/*.ts"] +} diff --git a/templates/app-ts-esm/tsconfig.json b/templates/app-ts-esm/tsconfig.json new file mode 100644 index 00000000..944d2e6e --- /dev/null +++ b/templates/app-ts-esm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "fastify-tsconfig", + "compilerOptions": { + "outDir": "dist", + "sourceMap": true, + "moduleResolution": "Node16", + "module": "ES2022", + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +} diff --git a/test/generate-typescript-esm.test.js b/test/generate-typescript-esm.test.js new file mode 100644 index 00000000..9115a38e --- /dev/null +++ b/test/generate-typescript-esm.test.js @@ -0,0 +1,192 @@ +'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, execSync } = require('child_process') +const minimatch = require('minimatch') +const strip = require('strip-ansi') +const expected = {} +const initVersion = execSync('npm get init-version').toString().trim() + +;(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 --esm', (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, initVersion) + 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].replace(/"/g, '') + + 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) + }) + }) + } +}