diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 541474d..81ca95d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,6 +29,9 @@ jobs: - name: npm test run: npm test + - name: npm types + run: npm run types + - name: npx semantic-release run: npx semantic-release env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c1b562..1882a67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,3 +32,6 @@ jobs: - name: npm test run: npm test + + - name: npm types + run: npm run types diff --git a/.gitignore b/.gitignore index 671a52f..04e892c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ coverage/ node_modules/ *.log .vscode -.tap \ No newline at end of file +.tap +*.d.ts +!podium.d.ts diff --git a/README.md b/README.md index c0bb092..23028c4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ const app = fastify(); const podlet = new Podlet({ pathname: '/', version: '2.0.0', - name: 'podletContent', + name: 'podlet-content', }); // Register the plugin, with the podlet as the option @@ -44,21 +44,24 @@ app.get(podlet.content(), async (request, reply) => { return; } reply.podiumSend('

Hello world

'); + await reply; }); app.get(podlet.manifest(), async (request, reply) => { reply.send(podlet); + await reply; }); const start = async () => { try { - await app.listen(7100); + await app.listen({ port: 7100 }); app.log.info(`server listening on ${app.server.address().port}`); } catch (err) { app.log.error(err); process.exit(1); } }; + start(); ``` @@ -86,6 +89,7 @@ app.get(podlet.content(), async (request, reply) => { return; } reply.podiumSend('

Hello world

'); + await reply; }); ``` diff --git a/example/server.js b/example/server.js deleted file mode 100644 index 6a9abc5..0000000 --- a/example/server.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const fastifyPodletPlugin = require('../'); -const fastify = require('fastify'); -const Podlet = require('@podium/podlet'); - -const app = fastify({ logger: true }); - -const podlet = new Podlet({ - pathname: '/', - fallback: '/fallback', - version: `2.0.0-${Date.now().toString()}`, - logger: console, - name: 'podletContent', - development: true, -}); - -podlet.defaults({ - locale: 'nb-NO', -}); - -app.register(fastifyPodletPlugin, podlet); - -app.get(podlet.content(), async (request, reply) => { - if (reply.app.podium.context.locale === 'nb-NO') { - reply.podiumSend('

Hei verden

'); - return; - } - reply.podiumSend('

Hello world

'); -}); - -app.get(podlet.fallback(), async (request, reply) => { - reply.podiumSend('

We are sorry but we can not display this!

'); -}); - -app.get(podlet.manifest(), async (request, reply) => { - reply.send(podlet); -}); - -app.get('/public', async (request, reply) => { - reply.send({ say: 'Hello world' }); -}); - -// Test URL: http://localhost:7100/podium-resource/podletContent/localApi -podlet.proxy({ target: '/public', name: 'localApi' }); -// Test URL: http://localhost:7100/podium-resource/podletContent/remoteApi -podlet.proxy({ target: 'https://api.ipify.org', name: 'remoteApi' }); - -// Run the server! -const start = async () => { - try { - await app.listen(7100); - app.log.info(`server listening on ${app.server.address().port}`); - } catch (err) { - app.log.error(err); - process.exit(1); - } -}; -start(); diff --git a/fixup.js b/fixup.js new file mode 100755 index 0000000..7fd5625 --- /dev/null +++ b/fixup.js @@ -0,0 +1,12 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +let podium = path.join(process.cwd(), 'types', 'podium.d.ts'); +let module = path.join(process.cwd(), 'types', 'podlet-plugin.d.ts'); + +fs.writeFileSync( + module, + `${fs.readFileSync(podium, 'utf-8')} +${fs.readFileSync(module, 'utf-8')}`, + 'utf-8', +); diff --git a/lib/podlet-plugin.js b/lib/podlet-plugin.js index 43c461f..dbd5a3f 100644 --- a/lib/podlet-plugin.js +++ b/lib/podlet-plugin.js @@ -1,68 +1,85 @@ import { HttpIncoming, pathnameBuilder } from '@podium/utils'; import fp from 'fastify-plugin'; -const podiumPodletFastifyPlugin = (fastify, podlet, done) => { - // Decorate reply with .app.podium we can write to throught the request - fastify.decorateReply('app', null); - fastify.addHook('onRequest', async (request, reply) => { - reply.app = { - podium: {}, - }; - }); - - // Run parsers on pre handler and store state object on reply.app.podium - fastify.addHook('preHandler', async (request, reply) => { - const incoming = new HttpIncoming( - request.raw, - reply.raw, - reply.app.params, - ); - reply.app.podium = await podlet.process(incoming, { proxy: false }); - }); - - // Set http headers on response - fastify.addHook('preHandler', async (request, reply) => { - reply.header('podlet-version', podlet.version); - }); - - // Decorate response with .podiumSend() method - fastify.decorateReply('podiumSend', function podiumSend(payload) { - this.type('text/html; charset=utf-8'); // "this" here is the fastify 'Reply' object - this.send(podlet.render(this.app.podium, payload)); - }); - - // Mount proxy route as an instance so its executed only on - // the registered path. Iow: the proxy check is not run on - // any other routes - fastify.register((instance, opts, next) => { - const pathname = pathnameBuilder( - podlet.httpProxy.pathname, - podlet.httpProxy.prefix, - '/*', - ); +export default fp( + /** + * @type {import('fastify').FastifyPluginCallback} + */ + (fastify, podlet, done) => { + // Decorate reply with .app.podium we can write to throught the request + fastify.decorateReply('app', null); + fastify.addHook('onRequest', async (request, reply) => { + // @ts-ignore We decorate this above + reply.app = { + podium: {}, + }; + }); - // Allow all content types for proxy requests - // https://github.com/fastify/fastify/blob/main/docs/ContentTypeParser.md#catch-all - instance.addContentTypeParser('*', (req, payload, cb) => { - cb(); + // Run parsers on pre handler and store state object on reply.app.podium + fastify.addHook('preHandler', async (request, reply) => { + const incoming = new HttpIncoming( + request.raw, + reply.raw, + // @ts-ignore + reply.app.params, + ); + // @ts-ignore We decorate this above + reply.app.podium = await podlet.process(incoming, { proxy: false }); }); - instance.addHook('preHandler', async (req, reply) => { - const incoming = await podlet.httpProxy.process(reply.app.podium); - if (incoming.proxy) return; - return incoming; + // Set http headers on response + fastify.addHook('preHandler', async (request, reply) => { + reply.header('podlet-version', podlet.version); }); - instance.all(pathname, (req, reply) => { - reply.code(404).send('Not found'); + // Decorate response with .podiumSend() method + fastify.decorateReply('podiumSend', function podiumSend(payload) { + this.type('text/html; charset=utf-8'); // "this" here is the fastify 'Reply' object + this.send( + podlet.render( + // @ts-ignore We decorate this above + this.app.podium, + payload, + ), + ); }); - next(); - }); + // Mount proxy route as an instance so its executed only on + // the registered path. Iow: the proxy check is not run on + // any other routes + fastify.register((instance, opts, next) => { + const pathname = pathnameBuilder( + podlet.httpProxy.pathname, + podlet.httpProxy.prefix, + '/*', + ); - done(); -}; + // Allow all content types for proxy requests + // https://github.com/fastify/fastify/blob/main/docs/ContentTypeParser.md#catch-all + instance.addContentTypeParser('*', (req, payload, cb) => { + // @ts-ignore + cb(); + }); + + instance.addHook('preHandler', async (req, reply) => { + const incoming = await podlet.httpProxy.process( + // @ts-ignore We decorate this above + reply.app.podium, + ); + if (incoming.proxy) return; + return incoming; + }); + + instance.all(pathname, (req, reply) => { + reply.code(404).send('Not found'); + }); + + next(); + }); -export default fp(podiumPodletFastifyPlugin, { - name: 'podium-podlet', -}); + done(); + }, + { + name: 'podium-podlet', + }, +); diff --git a/package.json b/package.json index 82b9de7..3808dd3 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "description": "Fastify plugin for Podium Podlet", "main": "lib/podlet-plugin.js", + "types": "types/podlet-plugin.d.ts", "repository": { "type": "git", "url": "git@github.com:podium-lib/fastify-podlet.git" @@ -17,13 +18,18 @@ "CHANGELOG.md", "README.md", "LICENSE", - "lib" + "lib", + "types" ], "scripts": { "test": "tap --disable-coverage --allow-empty-coverage", "test:coverage": "tap", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "types": "run-s types:tsc types:fixup", + "types:tsc": "tsc", + "types:test": "tsc --project tsconfig.test.json", + "types:fixup": "node ./fixup.js" }, "author": "Trygve Lie", "license": "MIT", @@ -40,7 +46,7 @@ }, "devDependencies": { "@fastify/formbody": "7.4.0", - "@podium/podlet": "5.1.12", + "@podium/podlet": "5.1.17", "@podium/test-utils": "2.5.2", "@semantic-release/changelog": "6.0.3", "@semantic-release/commit-analyzer": "11.1.0", @@ -53,8 +59,10 @@ "eslint-plugin-prettier": "5.2.1", "fastify": "4.28.1", "globals": "15.9.0", + "npm-run-all2": "6.2.3", "prettier": "3.3.3", "semantic-release": "23.1.1", - "tap": "18.8.0" + "tap": "18.8.0", + "typescript": "5.6.2" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..076be6a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "module": "nodenext", + "target": "es2020", + "resolveJsonModule": true, + "checkJs": true, + "allowJs": true, + "moduleResolution": "nodenext", + "emitDeclarationOnly": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "outDir": "types" + }, + "include": ["./lib/**/*.js"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..530ad59 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["./tests/**/*.js"], + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "noEmit": true + } +} diff --git a/types/podium.d.ts b/types/podium.d.ts new file mode 100644 index 0000000..2c56b86 --- /dev/null +++ b/types/podium.d.ts @@ -0,0 +1,42 @@ +import { HttpIncoming } from '@podium/utils'; + +declare module 'fastify' { + + + interface PodiumHttpIncomingParameters { + [key: string]: unknown; + } + + // @podium/podlet declares what's on the context. We use the same interface names here to inherit them. + + interface PodiumHttpIncomingContext { + [key: string]: unknown; + } + + interface PodiumHttpIncomingViewParameters { + [key: string]: unknown; + } + + interface PodiumLocals { + podium: HttpIncoming; + } + + interface FastifyReply { + app: PodiumLocals; + + /** + * Calls the send / write method on the `http.ServerResponse` object. + * + * When in development mode this method will wrap the provided fragment in a + * default HTML document before dispatching. When not in development mode, this + * method will just dispatch the fragment. + * + * @example + * app.get(podlet.content(), async (req, reply) => { + * reply.podiumSend('

Hello World

'); + * await reply; + * }); + */ + podiumSend(fragment: string, ...args: unknown[]): Response; + } +}