├── .github └── workflows │ └── run-tests.yml ├── LICENSE ├── README.md ├── lerna.json ├── nx.json ├── package.json ├── packages ├── application │ ├── package.json │ ├── src │ │ ├── application.ts │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── test │ │ ├── application.js │ │ └── fixtures │ │ │ ├── bootstrap │ │ │ ├── providers.js │ │ │ └── test-service-provider.js │ │ │ ├── config │ │ │ ├── ignored.txt │ │ │ ├── test.d.ts │ │ │ └── test.js │ │ │ └── package.json │ └── tsconfig.json ├── config │ ├── package.json │ ├── src │ │ ├── config.ts │ │ └── index.ts │ ├── test │ │ ├── config.js │ │ └── fixtures │ │ │ └── config │ │ │ ├── app.js │ │ │ └── test.js │ └── tsconfig.json ├── console │ ├── package.json │ ├── src │ │ ├── application.ts │ │ ├── command.ts │ │ ├── index.ts │ │ └── sample-command.ts │ ├── test │ │ └── index.js │ └── tsconfig.json ├── container │ ├── package.json │ ├── src │ │ ├── container.ts │ │ └── index.ts │ ├── test │ │ └── index.js │ └── tsconfig.json ├── contracts │ ├── package.json │ ├── src │ │ ├── application │ │ │ ├── application.ts │ │ │ └── config.ts │ │ ├── config │ │ │ └── store.ts │ │ ├── console │ │ │ ├── application.ts │ │ │ ├── command.ts │ │ │ └── kernel.ts │ │ ├── container │ │ │ └── container.ts │ │ ├── core │ │ │ ├── bootstrapper.ts │ │ │ ├── error-handler.ts │ │ │ ├── renderable-error.ts │ │ │ └── reportable-error.ts │ │ ├── database │ │ │ ├── config.ts │ │ │ └── database.ts │ │ ├── encryption │ │ │ └── encrypter.ts │ │ ├── env │ │ │ └── env.ts │ │ ├── hashing │ │ │ ├── base-hasher.ts │ │ │ ├── config.ts │ │ │ ├── hash-algorithms.ts │ │ │ ├── hash-builder.ts │ │ │ └── hasher.ts │ │ ├── http │ │ │ ├── bodyparser-config.ts │ │ │ ├── concerns │ │ │ │ ├── interacts-with-content-types.ts │ │ │ │ ├── interacts-with-state.ts │ │ │ │ └── state-bag.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── controller.ts │ │ │ ├── cookie-bag.ts │ │ │ ├── cookie-config-builder.ts │ │ │ ├── cookie-config.ts │ │ │ ├── cors-config.ts │ │ │ ├── file-bag.ts │ │ │ ├── input-bag.ts │ │ │ ├── kernel.ts │ │ │ ├── methods.ts │ │ │ ├── middleware.ts │ │ │ ├── pending-route.ts │ │ │ ├── query-parameter-bag.ts │ │ │ ├── redirect.ts │ │ │ ├── request-headers.ts │ │ │ ├── request.ts │ │ │ ├── response.ts │ │ │ ├── route-collection.ts │ │ │ ├── route-group.ts │ │ │ ├── route.ts │ │ │ ├── router.ts │ │ │ ├── server.ts │ │ │ ├── static-assets-config.ts │ │ │ └── uploaded-file.ts │ │ ├── index.ts │ │ ├── logging │ │ │ ├── config.ts │ │ │ └── logger.ts │ │ ├── queue │ │ │ ├── database-queue.ts │ │ │ ├── job.ts │ │ │ └── queue.ts │ │ ├── session │ │ │ ├── config.ts │ │ │ ├── driver.ts │ │ │ └── session.ts │ │ ├── support │ │ │ ├── htmlable.ts │ │ │ └── service-provider.ts │ │ ├── utils │ │ │ ├── class.ts │ │ │ └── object.ts │ │ ├── view │ │ │ ├── config-builder.ts │ │ │ ├── config.ts │ │ │ ├── engine.ts │ │ │ ├── response-config.ts │ │ │ └── response.ts │ │ └── vite │ │ │ └── config.ts │ └── tsconfig.json ├── core │ ├── package.json │ ├── src │ │ ├── application.ts │ │ ├── bootstrappers │ │ │ ├── boot-service-providers.ts │ │ │ ├── handle-exceptions.ts │ │ │ ├── handle-shutdown.ts │ │ │ ├── index.ts │ │ │ ├── load-configuration.ts │ │ │ ├── load-environment-variables.ts │ │ │ └── register-service-providers.ts │ │ ├── console │ │ │ └── kernel.ts │ │ ├── errors │ │ │ ├── environment-file-error.ts │ │ │ ├── handler.ts │ │ │ ├── http-error.ts │ │ │ └── index.ts │ │ ├── http │ │ │ └── kernel.ts │ │ ├── index.ts │ │ ├── providers │ │ │ ├── index.ts │ │ │ └── route-service-provider.ts │ │ └── shutdown-signal-listener.ts │ ├── test │ │ ├── console-kernel.js │ │ ├── error-handler.js │ │ ├── fixtures │ │ │ ├── .env │ │ │ ├── .env.testing │ │ │ ├── app │ │ │ │ └── console │ │ │ │ │ └── commands │ │ │ │ │ └── test-command.js │ │ │ ├── bootstrap │ │ │ │ └── providers.js │ │ │ ├── config │ │ │ │ ├── ignored.txt │ │ │ │ ├── test.d.ts │ │ │ │ └── test.js │ │ │ ├── package.json │ │ │ ├── resources │ │ │ │ └── views │ │ │ │ │ └── errors │ │ │ │ │ └── 401.hbs │ │ │ └── routes │ │ │ │ └── web.js │ │ ├── http-kernel.js │ │ └── route-service-provider.js │ └── tsconfig.json ├── database │ ├── package.json │ ├── src │ │ ├── database-manager-proxy.ts │ │ ├── database-manager.ts │ │ ├── database-service-provider.ts │ │ ├── index.ts │ │ ├── model.ts │ │ └── query-builder.ts │ ├── test │ │ ├── database-service-provider.js │ │ ├── database.js │ │ ├── helpers │ │ │ ├── index.js │ │ │ ├── post-model.js │ │ │ └── user-model.js │ │ ├── model.js │ │ └── query-builder.js │ └── tsconfig.json ├── encryption │ ├── package.json │ ├── src │ │ ├── encrypter.ts │ │ ├── encryption-service-provider.ts │ │ └── index.ts │ ├── test │ │ ├── encrypter.js │ │ └── encryption-service-provider.js │ └── tsconfig.json ├── env │ ├── package.json │ ├── src │ │ ├── env.ts │ │ └── index.ts │ ├── test │ │ └── env.js │ └── tsconfig.json ├── facades │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── config.ts │ │ ├── crypt.ts │ │ ├── database.ts │ │ ├── env.ts │ │ ├── facade.ts │ │ ├── hash.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── route.ts │ │ └── view.ts │ ├── test │ │ └── index.js │ └── tsconfig.json ├── hashing │ ├── package.json │ ├── src │ │ ├── base-hasher.ts │ │ ├── bcrypt-hasher.ts │ │ ├── hash-builder.ts │ │ ├── hash-manager.ts │ │ ├── hashing-service-provider.ts │ │ ├── index.ts │ │ ├── missing-hasher-error.ts │ │ ├── scrypt-hasher.ts │ │ └── scrypt-validation-error.ts │ ├── test │ │ ├── base-hasher.js │ │ ├── bcrypt-hasher.js │ │ ├── hash-manager.js │ │ ├── hashing-service-provider.js │ │ ├── helpers │ │ │ └── index.js │ │ └── scrypt-hasher.js │ └── tsconfig.json ├── http │ ├── package.json │ ├── src │ │ ├── http-service-provider.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── base.ts │ │ │ ├── bodyparser │ │ │ │ ├── bodyparser-base-options.ts │ │ │ │ ├── bodyparser-json-options.ts │ │ │ │ ├── bodyparser-multipart-options.ts │ │ │ │ ├── bodyparser-options.ts │ │ │ │ ├── bodyparser.ts │ │ │ │ └── index.ts │ │ │ ├── handle-cors.ts │ │ │ ├── handle-error.ts │ │ │ ├── index.ts │ │ │ └── serve-static-assets.ts │ │ ├── routing │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── pending-route.ts │ │ │ ├── route-collection.ts │ │ │ ├── route.ts │ │ │ └── router.ts │ │ └── server │ │ │ ├── controller.ts │ │ │ ├── cookie-bag.ts │ │ │ ├── cookies │ │ │ ├── index.ts │ │ │ ├── request-cookie-builder.ts │ │ │ └── response-cookie-builder.ts │ │ │ ├── file-bag.ts │ │ │ ├── http-context.ts │ │ │ ├── http-redirect.ts │ │ │ ├── index.ts │ │ │ ├── input-bag.ts │ │ │ ├── interacts-with-state.ts │ │ │ ├── query-parameter-bag.ts │ │ │ ├── request-header-bag.ts │ │ │ ├── request.ts │ │ │ ├── response-header-bag.ts │ │ │ ├── response.ts │ │ │ ├── server.ts │ │ │ ├── state-bag.ts │ │ │ └── uploaded-file.ts │ ├── test │ │ ├── controller.js │ │ ├── file-bag.js │ │ ├── helpers │ │ │ ├── error-handler.js │ │ │ └── index.js │ │ ├── http-context.js │ │ ├── http-service-provider.js │ │ ├── input-bag.js │ │ ├── middleware │ │ │ ├── bodyparser │ │ │ │ ├── bodyparser-json-options.js │ │ │ │ ├── bodyparser-multipart-options.js │ │ │ │ ├── bodyparser-options.js │ │ │ │ ├── bodyparser-text-options.js │ │ │ │ ├── bodyparser.js │ │ │ │ └── fixtures │ │ │ │ │ ├── bodyparser-config.js │ │ │ │ │ ├── test-multipart-file-1.txt │ │ │ │ │ └── test-multipart-file-2.txt │ │ │ ├── handle-cors │ │ │ │ ├── fixtures │ │ │ │ │ └── cors-config.js │ │ │ │ └── handle-cors.js │ │ │ └── serve-static-assets │ │ │ │ ├── fixtures │ │ │ │ ├── public │ │ │ │ │ ├── index.html │ │ │ │ │ └── style.css │ │ │ │ └── static-assets.js │ │ │ │ └── serve-static-assets.js │ │ ├── pending-route.js │ │ ├── redirect.js │ │ ├── request.js │ │ ├── response.js │ │ ├── route.js │ │ ├── router.js │ │ ├── server.js │ │ └── state.js │ └── tsconfig.json ├── logging │ ├── package.json │ ├── src │ │ ├── console-logger.ts │ │ ├── contracts.ts │ │ ├── file-logger.ts │ │ ├── index.ts │ │ ├── log-manager.ts │ │ ├── logger.ts │ │ └── logging-service-provider.ts │ ├── test │ │ ├── console-logger.js │ │ └── file-logger.js │ └── tsconfig.json ├── manager │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── manager.ts │ ├── test │ │ └── manager.js │ └── tsconfig.json ├── session │ ├── package.json │ ├── src │ │ ├── drivers │ │ │ ├── cookie.ts │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ └── memory.ts │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── index.ts │ │ │ ├── start-session.ts │ │ │ └── verify-csrf-token.ts │ │ ├── session-config-cookie.ts │ │ ├── session-config.ts │ │ ├── session-manager.ts │ │ ├── session-service-provider.ts │ │ └── session.ts │ ├── test │ │ ├── cookie-session-driver.js │ │ ├── file-session-driver.js │ │ ├── helpers │ │ │ ├── error-handler.js │ │ │ └── index.js │ │ ├── memory-session-driver.js │ │ ├── session-config.js │ │ ├── session-flash-messages.js │ │ ├── session-manager.js │ │ ├── session-service-provider.js │ │ ├── session.js │ │ └── verify-csrf-token-middleware.js │ └── tsconfig.json ├── support │ ├── package.json │ ├── src │ │ ├── html-string.ts │ │ ├── index.ts │ │ ├── interacts-with-time.ts │ │ └── service-provider.ts │ ├── test │ │ ├── fixtures │ │ │ ├── test-config-without-default-export.js │ │ │ └── test-config.js │ │ ├── html-string.js │ │ ├── interacts-with-time.js │ │ └── service-provider.js │ └── tsconfig.json ├── view │ ├── package.json │ ├── src │ │ ├── drivers │ │ │ ├── base-driver.ts │ │ │ ├── handlebars │ │ │ │ ├── handlebars-driver.ts │ │ │ │ ├── helpers │ │ │ │ │ ├── append.ts │ │ │ │ │ ├── json.ts │ │ │ │ │ ├── prepend.ts │ │ │ │ │ ├── raw.ts │ │ │ │ │ └── stack.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── view-config-builder.ts │ │ ├── view-manager.ts │ │ ├── view-response.ts │ │ └── view-service-provider.ts │ ├── test │ │ ├── fixtures │ │ │ └── resources │ │ │ │ └── views │ │ │ │ ├── helpers │ │ │ │ └── test-helper.js │ │ │ │ ├── layouts │ │ │ │ └── test.hbs │ │ │ │ ├── partials │ │ │ │ └── test-partial.hbs │ │ │ │ ├── test-view-helper-json-pretty.hbs │ │ │ │ ├── test-view-helper-json.hbs │ │ │ │ ├── test-view-helper-raw.hbs │ │ │ │ ├── test-view-helper-stack-append-prepend.hbs │ │ │ │ ├── test-view-helper-stack.hbs │ │ │ │ ├── test-view-with-in-memory-helper.hbs │ │ │ │ ├── test-view-with-in-memory-partial.hbs │ │ │ │ ├── test-view-with-partial.hbs │ │ │ │ └── test-view.hbs │ │ ├── helpers │ │ │ └── index.js │ │ ├── view-helper.js │ │ ├── view-manager.js │ │ └── view-service-provider.js │ └── tsconfig.json └── vite │ ├── package.json │ ├── src │ ├── backend │ │ ├── vite-config.ts │ │ ├── vite-handlebars-helper.ts │ │ ├── vite-manifest.ts │ │ └── vite.ts │ ├── index.ts │ ├── inertia │ │ ├── inertia-helpers.ts │ │ └── inertia-page-not-found-error.ts │ ├── plugin │ │ ├── hotfile.ts │ │ ├── plugin.ts │ │ └── types.ts │ └── vite-service-provider.ts │ ├── test │ ├── fixtures │ │ ├── resources │ │ │ └── views │ │ │ │ ├── test-vite-helper-hash-arguments-number.hbs │ │ │ │ ├── test-vite-helper-hash-arguments.hbs │ │ │ │ ├── test-vite-helper-unnamed-arguments.hbs │ │ │ │ └── test-vite-helper-with-attributes.hbs │ │ └── test-page.js │ ├── helpers │ │ └── index.js │ ├── inertia-helpers.js │ ├── vite-config.js │ ├── vite-plugin.js │ ├── vite-service-provider.js │ └── vite.js │ └── tsconfig.json └── tsconfig.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright (c) Marcus Pöhls 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.0-alpha.2", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "command": { 7 | "publish": { 8 | "ignoreChanges": [ 9 | "packages/app/", 10 | "package-lock.json" 11 | ] 12 | }, 13 | "bootstrap": { 14 | "npmClientArgs": [ 15 | "--no-package-lock" 16 | ] 17 | } 18 | }, 19 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 20 | } 21 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": [ 7 | "build", 8 | "test" 9 | ] 10 | } 11 | } 12 | }, 13 | "pluginsConfig": { 14 | "@nrwl/js": { 15 | "analyzeSourceFiles": false 16 | } 17 | }, 18 | "extends": "nx/presets/npm.json", 19 | "affected": { 20 | "defaultBase": "main" 21 | }, 22 | "namedInputs": { 23 | "sharedGlobals": [ 24 | "{workspaceRoot}/.eslintrc.json" 25 | ] 26 | }, 27 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 28 | "targetDefaults": { 29 | "build": { 30 | "cache": true 31 | }, 32 | "test": { 33 | "cache": true 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/framework", 3 | "description": "Supercharge Node.js framework", 4 | "version": "1.0.0-beta9", 5 | "author": "Marcus Pöhls ", 6 | "type": "module", 7 | "bugs": { 8 | "url": "https://github.com/supercharge/framework/issues" 9 | }, 10 | "devDependencies": { 11 | "@supercharge/eslint-config-typescript": "~4.0.1", 12 | "@supercharge/tsconfig": "~7.0.0", 13 | "@types/node": "~20.10.5", 14 | "eslint": "~8.56.0", 15 | "lerna": "~8.0.1", 16 | "nx": "17.1.2" 17 | }, 18 | "engines": { 19 | "node": ">=22" 20 | }, 21 | "homepage": "https://superchargejs.com", 22 | "keywords": [ 23 | "supercharge", 24 | "superchargejs", 25 | "node", 26 | "node.js", 27 | "framework", 28 | "javascript" 29 | ], 30 | "license": "MIT", 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/supercharge/framework.git" 37 | }, 38 | "scripts": { 39 | "build": "lerna run build", 40 | "clean": "lerna clean --yes", 41 | "dev": "lerna run dev", 42 | "fresh": "npm run clean && npm install", 43 | "lint": "lerna run lint", 44 | "publish": "npm run release", 45 | "release": "npm run build && lerna publish --force-publish", 46 | "test": "lerna run test", 47 | "test:nocache": "npm test -- --skip-nx-cache", 48 | "watch": "npm run dev" 49 | }, 50 | "workspaces": [ 51 | "packages/*" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /packages/application/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Application } from './application.js' 3 | -------------------------------------------------------------------------------- /packages/application/src/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { PackageJson } from 'type-fest' 3 | import { Arr } from '@supercharge/arrays' 4 | import { Application, ConfigStore, EnvStore, ServiceProvider } from '@supercharge/contracts' 5 | 6 | export type Callback = (app: Application) => unknown | Promise 7 | 8 | export interface ApplicationMeta { 9 | /** 10 | * The absolute path to the application’s root directory. 11 | */ 12 | appRoot: string 13 | 14 | /** 15 | * The config store instance. 16 | */ 17 | config: ConfigStore 18 | 19 | /** 20 | * The env store instance. 21 | */ 22 | env: EnvStore 23 | 24 | /** 25 | * The environment file to load during application bootstrapping. 26 | */ 27 | environmentFile: string 28 | 29 | /** 30 | * The directory for the environment file. 31 | */ 32 | environmentPath?: string 33 | 34 | /** 35 | * Indicate whether the application runs in the console. 36 | */ 37 | isRunningInConsole: boolean 38 | 39 | /** 40 | * All booting callbacks. 41 | */ 42 | bootingCallbacks: Callback[] 43 | 44 | /** 45 | * Shutdown callbacks run when the application stops. We’re 46 | * using this in the framework to u 47 | */ 48 | shutdownCallbacks: Callback[] 49 | 50 | /** 51 | * All registered service providers. 52 | */ 53 | serviceProviders: Arr 54 | 55 | /** 56 | * The the application’s `package.json` content. 57 | */ 58 | packageJson: PackageJson | undefined 59 | } 60 | -------------------------------------------------------------------------------- /packages/application/test/fixtures/bootstrap/providers.js: -------------------------------------------------------------------------------- 1 | 2 | import TestServiceProvider from './test-service-provider.js' 3 | 4 | export const providers = [ 5 | TestServiceProvider 6 | ] 7 | -------------------------------------------------------------------------------- /packages/application/test/fixtures/bootstrap/test-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | export default class TestServiceProvider { 3 | register (app) { 4 | app.bind('test-register', () => { 5 | return true 6 | }) 7 | } 8 | 9 | boot (app) { 10 | app.bind('test-boot', () => { 11 | return true 12 | }) 13 | } 14 | 15 | /** 16 | * Both functions are placeholders which are implemented in the `ServiceProvider` 17 | * base class from the @supercharge/support package. 18 | */ 19 | callBootingCallbacks () {} 20 | callBootedCallbacks () {} 21 | } 22 | -------------------------------------------------------------------------------- /packages/application/test/fixtures/config/ignored.txt: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | foo: 'bar' 4 | } 5 | -------------------------------------------------------------------------------- /packages/application/test/fixtures/config/test.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercharge/framework/21992528dd0ba30b3a1b505e14125d77b7d365f6/packages/application/test/fixtures/config/test.d.ts -------------------------------------------------------------------------------- /packages/application/test/fixtures/config/test.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | foo: 'bar' 4 | } 5 | -------------------------------------------------------------------------------- /packages/application/test/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-test-fixtures", 3 | "version": "1.2.3", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /packages/application/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../config" 17 | }, 18 | { 19 | "path": "../container" 20 | }, 21 | { 22 | "path": "../contracts" 23 | }, 24 | { 25 | "path": "../env" 26 | }, 27 | { 28 | "path": "../logging" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/config", 3 | "description": "The Supercharge config package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "lodash": "~4.17.21" 21 | }, 22 | "devDependencies": { 23 | "@types/lodash": "~4.14.202", 24 | "c8": "~9.1.0", 25 | "expect": "~29.7.0", 26 | "typescript": "~5.4.5", 27 | "uvu": "~0.5.6" 28 | }, 29 | "engines": { 30 | "node": ">=22" 31 | }, 32 | "homepage": "https://superchargejs.com", 33 | "keywords": [ 34 | "config", 35 | "supercharge", 36 | "superchargejs", 37 | "nodejs" 38 | ], 39 | "license": "MIT", 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "directory": "packages/config", 46 | "url": "git+https://github.com/supercharge/framework.git" 47 | }, 48 | "scripts": { 49 | "build": "tsc --build tsconfig.json --force", 50 | "dev": "npm run build -- --watch", 51 | "watch": "npm run dev", 52 | "lint": "eslint src --ext .js,.ts", 53 | "lint:fix": "npm run lint -- --fix", 54 | "test": "npm run build && npm run lint && npm run test:run", 55 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 56 | "posttest": "c8 report --reporter=html" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/config/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Config } from './config.js' 3 | -------------------------------------------------------------------------------- /packages/config/test/fixtures/config/app.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | name: 'Supercharge Config', 4 | nested: { 5 | key: 'nested-value' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/config/test/fixtures/config/test.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | testing: true 4 | } 5 | -------------------------------------------------------------------------------- /packages/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/console", 3 | "description": "The Supercharge console package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "types": "dist", 12 | "exports": { 13 | ".": "./dist/index.js" 14 | }, 15 | "dependencies": { 16 | "@supercharge/cedar": "~2.0.0", 17 | "@supercharge/contracts": "^4.0.0-alpha.2" 18 | }, 19 | "devDependencies": { 20 | "c8": "~9.1.0", 21 | "expect": "~29.7.0", 22 | "typescript": "~5.4.5", 23 | "uvu": "~0.5.6" 24 | }, 25 | "engines": { 26 | "node": ">=22" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "homepage": "https://superchargejs.com", 32 | "keywords": [ 33 | "config", 34 | "supercharge", 35 | "superchargejs", 36 | "nodejs" 37 | ], 38 | "license": "MIT", 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "directory": "packages/console", 45 | "url": "git+https://github.com/supercharge/framework.git" 46 | }, 47 | "scripts": { 48 | "build": "tsc --build tsconfig.json --force", 49 | "dev": "npm run build -- --watch", 50 | "watch": "npm run build -- --watch", 51 | "lint": "eslint src --ext .js,.ts", 52 | "lint:fix": "npm run lint -- --fix", 53 | "test": "npm run build && npm run lint && npm run test:run", 54 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 55 | "posttest": "c8 report --reporter=html" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/console/src/application.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Command } from './command.js' 3 | import { Application as CedarApplication } from '@supercharge/cedar' 4 | import { ConsoleApplication as ConsoleApplicationContract, Application as SuperchargeApp } from '@supercharge/contracts' 5 | 6 | export class Application extends CedarApplication implements ConsoleApplicationContract { 7 | /** 8 | * The Supercharge application instance. 9 | */ 10 | protected readonly supercharge: SuperchargeApp 11 | 12 | /** 13 | * Create a new console application instance. 14 | */ 15 | constructor (app: SuperchargeApp) { 16 | super(app.name()) 17 | 18 | app.markAsRunningInConsole() 19 | 20 | this.supercharge = app 21 | this.setVersion(app.version() ?? '') 22 | } 23 | 24 | /** 25 | * Add the given `command` to this application. 26 | */ 27 | override add (command: Command): this { 28 | if (typeof command.setSupercharge === 'function') { 29 | command.setSupercharge(this.supercharge) 30 | } 31 | 32 | return super.add(command) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/console/src/command.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from '@supercharge/contracts' 3 | import { Command as CedarCommand } from '@supercharge/cedar' 4 | 5 | export class Command extends CedarCommand { 6 | /** 7 | * The Supercharge application instance. 8 | */ 9 | private superchargeApp: Application | undefined 10 | 11 | /** 12 | * Returns the Supercharge application instance. This method is an alias for 13 | * the `supercharge()` method. This method aligns the console app with the 14 | * rest of the framework, because there’s the `.app()` method elsewhere. 15 | */ 16 | app (): Application { 17 | return this.supercharge() 18 | } 19 | 20 | /** 21 | * Returns the Supercharge application instance. 22 | */ 23 | supercharge (): Application { 24 | if (!this.superchargeApp) { 25 | throw new Error('Missing Supercharge application instance. You must set it via the `setSupercharge` method when registering this command') 26 | } 27 | 28 | return this.superchargeApp 29 | } 30 | 31 | /** 32 | * Set the supercharge application instance for this command. 33 | */ 34 | setSupercharge (app: Application): this { 35 | this.superchargeApp = app 36 | 37 | return this 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/console/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Application } from './application.js' 3 | export { Command } from './command.js' 4 | -------------------------------------------------------------------------------- /packages/console/src/sample-command.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Command } from './command.js' 3 | 4 | export class TestCommand extends Command { 5 | /** 6 | * Returns the command signature. 7 | */ 8 | static signature (): string { 9 | return ` 10 | make:model 11 | { file : this part after the colon is an argument description } 12 | { name? : this is an optional argument } 13 | { --target? : optional option, parsed as boolean } 14 | { --option-1 : optional option, parsed as boolean } 15 | { --no-config : negated optional option with alias, parsed as boolean and false by default because of the “no-" prefix } 16 | { --source= : required option and a value must be passed down when calling this command } 17 | ` 18 | } 19 | 20 | override async handle (): Promise { 21 | return true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/console/test/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | 5 | test.skip('TODO', () => { 6 | expect(true).toBeFalse() 7 | }) 8 | 9 | test.run() 10 | -------------------------------------------------------------------------------- /packages/console/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "./dist", 6 | "lib": [ 7 | "es2020" 8 | ], 9 | }, 10 | "include": [ 11 | "./**/*" 12 | ], 13 | "exclude": [ 14 | "dist", 15 | "node_modules" 16 | ], 17 | "references": [ 18 | { 19 | "path": "../contracts" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/container/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/container", 3 | "description": "The Supercharge container package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/classes": "~2.0.0", 20 | "@supercharge/contracts": "^4.0.0-alpha.2", 21 | "@supercharge/goodies": "~2.0.0", 22 | "@supercharge/map": "~1.5.0", 23 | "@supercharge/strings": "~2.0.0" 24 | }, 25 | "devDependencies": { 26 | "c8": "~9.1.0", 27 | "expect": "~29.7.0", 28 | "typescript": "~5.4.5", 29 | "uvu": "~0.5.6" 30 | }, 31 | "engines": { 32 | "node": ">=22" 33 | }, 34 | "homepage": "https://superchargejs.com", 35 | "keywords": [ 36 | "container", 37 | "ioc-container", 38 | "supercharge", 39 | "superchargejs", 40 | "nodejs" 41 | ], 42 | "license": "MIT", 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "directory": "packages/container", 49 | "url": "git+https://github.com/supercharge/framework.git" 50 | }, 51 | "scripts": { 52 | "build": "tsc --build tsconfig.json --force", 53 | "watch": "npm run build -- --watch", 54 | "lint": "eslint src --ext .js,.ts", 55 | "lint:fix": "npm run lint -- --fix", 56 | "test": "npm run build && npm run lint && npm run test:run", 57 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 58 | "posttest": "c8 report --reporter=html" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/container/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Container } from './container.js' 3 | -------------------------------------------------------------------------------- /packages/container/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/contracts", 3 | "description": "The Supercharge contracts package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "homepage": "https://superchargejs.com", 7 | "keywords": [ 8 | "supercharge", 9 | "superchargejs", 10 | "contracts" 11 | ], 12 | "license": "MIT", 13 | "files": [ 14 | "dist" 15 | ], 16 | "type": "module", 17 | "main": "dist/index.js", 18 | "types": "dist", 19 | "exports": { 20 | ".": "./dist/index.js" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "directory": "packages/contracts", 28 | "url": "git+https://github.com/supercharge/framework.git" 29 | }, 30 | "dependencies": { 31 | "@supercharge/macroable": "~2.0.1", 32 | "@types/koa__router": "~12.0.4", 33 | "@types/node": "~20.10.5", 34 | "handlebars": "~4.7.8", 35 | "knex": "~3.1.0" 36 | }, 37 | "devDependencies": { 38 | "typescript": "~5.4.5" 39 | }, 40 | "scripts": { 41 | "build": "tsc --build tsconfig.json --force", 42 | "dev": "npm run build -- --watch", 43 | "watch": "npm run dev" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/contracts/src/application/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ApplicationConfig { 3 | /** 4 | * The application name. 5 | */ 6 | name: string 7 | 8 | /** 9 | * The application key, used to encrypt data. 10 | */ 11 | key: string 12 | 13 | /** 14 | * The application description. 15 | */ 16 | description?: string 17 | 18 | /** 19 | * The application environment. 20 | */ 21 | env?: string 22 | 23 | /** 24 | * The application version. 25 | */ 26 | version?: string 27 | 28 | /** 29 | * Determine whether the application runs behind a proxy server. 30 | */ 31 | runsBehindProxy?: boolean 32 | } 33 | -------------------------------------------------------------------------------- /packages/contracts/src/console/application.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ConsoleApplication { 3 | /** 4 | * Runs the incoming console command for the given `input`. 5 | */ 6 | run(input: string[]): Promise 7 | } 8 | -------------------------------------------------------------------------------- /packages/contracts/src/console/command.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Command { 3 | /** 4 | * Returns the command signature. The command signature will be used to register the 5 | * console command. Ensure you're not using spaces in your command signatures. 6 | * Instead, use semicolons as separators, like `make:model`. 7 | */ 8 | signature (): string 9 | 10 | /** 11 | * Returns the command description displayed when calling the help overview. 12 | */ 13 | description (): string 14 | 15 | /** 16 | * Handle the console command. 17 | */ 18 | handle(...args: any[]): any 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/src/console/kernel.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ConsoleKernel { 3 | /** 4 | * Prepare the console application by running the configured bootstrappers. 5 | * This method doesn’t register configured commands in the application. 6 | * It prepares the console application which is useful for testing. 7 | */ 8 | prepare (): Promise 9 | 10 | /** 11 | * Bootstrap the console application to handle Craft commands. 12 | */ 13 | bootstrap(): Promise 14 | 15 | /** 16 | * Handle an incoming console command for the given `input`. 17 | */ 18 | run(input?: string[]): Promise 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/src/core/bootstrapper.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from '../index.js' 3 | 4 | export type BootstrapperCtor = 5 | /** 6 | * Create a new bootstrapper instance. 7 | */ 8 | new(app: Application) => Bootstrapper 9 | 10 | export interface Bootstrapper { 11 | /** 12 | * Bootstrap the given application. 13 | */ 14 | bootstrap(app: Application): Promise 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/src/core/error-handler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpContext, Application } from '../index.js' 3 | 4 | export type ErrorHandlerCtor = 5 | /** 6 | * Create a new error handler instance. 7 | */ 8 | new(app: Application) => ErrorHandler 9 | 10 | export interface ErrorHandler { 11 | /** 12 | * Tell the error handler to not report the `error` type. 13 | */ 14 | ignore(error: ErrorConstructor): this 15 | 16 | /** 17 | * Handle the given error. 18 | */ 19 | handle(error: Error, ctx: HttpContext): Promise 20 | 21 | /** 22 | * Report an error. 23 | */ 24 | report(error: Error, ctx: HttpContext): void | Promise 25 | 26 | /** 27 | * Render an error into an HTTP response. 28 | */ 29 | render(error: Error, ctx: HttpContext): Promise 30 | } 31 | -------------------------------------------------------------------------------- /packages/contracts/src/core/renderable-error.ts: -------------------------------------------------------------------------------- 1 | import { HttpContext } from '../index.js' 2 | 3 | export interface RenderableError extends Error { 4 | /** 5 | * Render an error into an HTTP response. 6 | */ 7 | render(error: Error, ctx: HttpContext): Promise | any 8 | } 9 | -------------------------------------------------------------------------------- /packages/contracts/src/core/reportable-error.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpContext } from '../index.js' 3 | 4 | export interface ReportableError extends Error { 5 | /** 6 | * Report an error, to a 3rd-party service, the console, a file, or somewhere else. 7 | */ 8 | report(error: Error, ctx: HttpContext): Promise | any 9 | } 10 | -------------------------------------------------------------------------------- /packages/contracts/src/database/config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Knex } from 'knex' 3 | 4 | export interface DatabaseConfig { 5 | /** 6 | * The default database connection name. 7 | * 8 | * @deprecated This `default` property is deprecated in favor of the `connection` property. 9 | */ 10 | default?: string 11 | 12 | /** 13 | * The default database connection name. 14 | */ 15 | connection: keyof this['connections'] 16 | 17 | /** 18 | * The settings of configured database connections. 19 | */ 20 | connections: Record 21 | } 22 | -------------------------------------------------------------------------------- /packages/contracts/src/database/database.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Knex } from 'knex' 3 | 4 | export type Database = Knex 5 | -------------------------------------------------------------------------------- /packages/contracts/src/encryption/encrypter.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Encrypter { 3 | /** 4 | * Encrypt the given `value` using the app key. 5 | */ 6 | encrypt(value: any): string 7 | 8 | /** 9 | * Decrypt the given `value` using the app key. 10 | */ 11 | decrypt(value: string): T | undefined 12 | } 13 | -------------------------------------------------------------------------------- /packages/contracts/src/hashing/base-hasher.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HashAlgorithm } from './hash-algorithms.js' 3 | import { HashBuilderCallback } from './hash-builder.js' 4 | import type { BinaryLike, Encoding, Hash } from 'node:crypto' 5 | 6 | export interface BaseHasher { 7 | /** 8 | * Creates and returns a Node.js `Hash` instance for the given `algorithm` 9 | * and the related `input` with (optional) `inputEncoding`. When `input` 10 | * is a string and `inputEncoding` is omitted, it defaults to `utf8`. 11 | */ 12 | createHash (algorithm: HashAlgorithm, input: string | BinaryLike, inputEncoding?: Encoding): Hash 13 | 14 | /** 15 | * Returns an MD5 hash instance for the given `content`. 16 | */ 17 | md5 (input: BinaryLike): string 18 | md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string 19 | md5 (input: string, inputEncoding: Encoding): Hash 20 | 21 | /** 22 | * Returns a SHA256 hash instance using SHA-2 for the given `content`. 23 | */ 24 | sha256 (input: BinaryLike): string 25 | sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string 26 | sha256 (input: string, inputEncoding: Encoding): Hash 27 | 28 | /** 29 | * Returns a SHA512 hash instance using SHA-2 for the given `content`. 30 | */ 31 | sha512 (input: BinaryLike): string 32 | sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string 33 | sha512 (input: string, inputEncoding: Encoding): Hash 34 | } 35 | -------------------------------------------------------------------------------- /packages/contracts/src/hashing/config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HasherCtor } from './hasher.js' 3 | 4 | export interface HashConfig { 5 | /** 6 | * The hashing driver name. 7 | */ 8 | driver: 'bcrypt' | 'scrypt' 9 | 10 | /** 11 | * The map of drivers and their constructors. 12 | */ 13 | drivers: Partial > 14 | 15 | /** 16 | * The bcrypt hashing config. 17 | */ 18 | bcrypt?: { 19 | /** 20 | * The number of rounds to use. 21 | */ 22 | rounds?: number 23 | } 24 | 25 | /** 26 | * The scrypt hashing config. 27 | * 28 | * @see https://nodejs.org/docs/latest-v18.x/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback 29 | */ 30 | scrypt?: { 31 | /** 32 | * The CPU/memory cost factor. Must be a power of two and greater than one. Default: 16384 33 | */ 34 | cost?: number 35 | 36 | /** 37 | * The block size parameter. Default: 8 38 | */ 39 | blockSize?: number 40 | 41 | /** 42 | * The salt size parameter in bytes. It’s recommended to use a salt at least 16 bytes long. Default: 16 43 | */ 44 | saltSize?: number 45 | 46 | /** 47 | * The desired key length in bytes. Default: 64 48 | */ 49 | keyLength?: number 50 | 51 | /** 52 | * The parallelization parameter. Default: 1 53 | */ 54 | parallelization?: number 55 | 56 | /** 57 | * The memory upper bound while generating the hash. Default: 16_777_216 (128 * costs * blockSize) 58 | */ 59 | maxMemory?: number 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/contracts/src/hashing/hash-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { BinaryToTextEncoding, Encoding } from 'node:crypto' 3 | 4 | export type HashBuilderCallback = (hashBuilder: HashBuilder) => unknown 5 | 6 | export interface HashBuilderConfig { 7 | inputEncoding?: Encoding 8 | outputEncoding: BinaryToTextEncoding 9 | } 10 | 11 | export interface HashBuilder { 12 | /** 13 | * Set the input encoding for the related value. 14 | */ 15 | inputEncoding(inputEncoding: Encoding): this 16 | 17 | /** 18 | * Calculate the final hash string value. 19 | */ 20 | digest (encoding: BinaryToTextEncoding): void 21 | 22 | /** 23 | * This is an alias for the `digest` method that calculates the final hash string. 24 | */ 25 | toString(outputEncoding: BinaryToTextEncoding): void 26 | } 27 | -------------------------------------------------------------------------------- /packages/contracts/src/hashing/hasher.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseHasher } from './base-hasher.js' 3 | 4 | export type HasherCtor = new (...args: any[]) => Hasher 5 | 6 | export interface Hasher extends BaseHasher { 7 | /** 8 | * Hash the given `value`. 9 | */ 10 | make (value: string): Promise 11 | 12 | /** 13 | * Compare a the `plain` text value against the given `hashedValue`. 14 | */ 15 | check (plain: string, hashedValue: string): Promise 16 | 17 | /** 18 | * Determine whether the given hash value has been hashed using the configured options. 19 | */ 20 | needsRehash (hashedValue: string): boolean 21 | } 22 | -------------------------------------------------------------------------------- /packages/contracts/src/http/concerns/interacts-with-state.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StateBag, HttpStateData } from './state-bag.js' 3 | 4 | export interface InteractsWithState { 5 | /** 6 | * Share a given `state` across HTTP requests. Any previously 7 | * set state will be merged with the given `state`. 8 | * 9 | * @example 10 | * ``` 11 | * response.share({ user: { id: 1, name: 'Marcus' } }) 12 | * ``` 13 | */ 14 | share (key: K, value: State[K]): this 15 | share (values: Partial): this 16 | share (key: string | any, value?: any): this 17 | 18 | /** 19 | * Returns the shared HTTP context state. 20 | * 21 | * @example 22 | * ``` 23 | * response.state() 24 | * 25 | * // something like "{ app: {…}, user: {…} }" 26 | * ``` 27 | */ 28 | state (): StateBag 29 | } 30 | -------------------------------------------------------------------------------- /packages/contracts/src/http/concerns/state-bag.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This interface defines key-value-pairs stored in the shared request state. 4 | * Extend this interface in a userland typing file and use TypeScript’s 5 | * declaration merging features to provide IntelliSense in the app. 6 | * 7 | * We’re not using a `Record`-like interface with an index signature, because 8 | * the index signature would resolve all keys to the `any` type. Using the 9 | * empty interface allows everyone to merge their interface definitions. 10 | */ 11 | 12 | import { InputBag } from '../input-bag.js' 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | export interface HttpStateData { 16 | // 17 | } 18 | 19 | export interface StateBag extends InputBag { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /packages/contracts/src/http/config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CookieConfig } from './cookie-config.js' 3 | 4 | export interface HttpConfig { 5 | /** 6 | * The HTTP server default host address or IP. Default’s to `localhost`. 7 | */ 8 | host: string 9 | 10 | /** 11 | * The local HTTP port. 12 | */ 13 | port: number 14 | 15 | /** 16 | * The HTTP cookie options. 17 | */ 18 | cookie: CookieConfig 19 | } 20 | -------------------------------------------------------------------------------- /packages/contracts/src/http/context.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpRequest } from './request.js' 3 | import { HttpResponse } from './response.js' 4 | import { RouterContext } from '@koa/router' 5 | import { InteractsWithState } from './concerns/interacts-with-state.js' 6 | 7 | export type NextHandler = () => any | Promise 8 | 9 | export interface HttpContext extends InteractsWithState { 10 | /** 11 | * Returns the raw Koa context. 12 | */ 13 | raw: RouterContext 14 | 15 | /** 16 | * Returns the HTTP request instance. 17 | */ 18 | request: HttpRequest 19 | 20 | /** 21 | * Returns the HTTP response instance. 22 | */ 23 | response: HttpResponse 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/http/controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpContext } from './context.js' 3 | 4 | export interface HttpController { 5 | /** 6 | * Handle the incoming HTTP request using the given `ctx`. 7 | */ 8 | handle(ctx: HttpContext): Promise | any 9 | } 10 | -------------------------------------------------------------------------------- /packages/contracts/src/http/cookie-bag.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RequestCookieBuilderCallback, ResponseCookieBuilderCallback } from './cookie-config-builder.js' 3 | 4 | export interface CookieBag { 5 | /** 6 | * Returns the cookie value for the given `name` if a cookie with that name 7 | * exists, `undefined` otherwise. Use a cookie options builder as the 8 | * second argument to customize the retrieval of the given cookie. 9 | */ 10 | get (name: string, cookieBuilder?: RequestCookieBuilderCallback): string | undefined 11 | 12 | /** 13 | * Set a response cookie with the given `name` and assign the `value`. 14 | */ 15 | set (name: string, value?: string, cookieBuilder?: ResponseCookieBuilderCallback): this 16 | 17 | /** 18 | * Determine whether a cookie exists for the given `name`. 19 | */ 20 | has(name: string): boolean 21 | 22 | /** 23 | * Removes a cookie with the given `name`. 24 | */ 25 | delete(name: string): this 26 | } 27 | -------------------------------------------------------------------------------- /packages/contracts/src/http/cookie-config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface CookieConfig { 3 | /** 4 | * The time or date in the future at which the cookie expires. 5 | */ 6 | expires?: Date 7 | 8 | /** 9 | * The number of milliseconds from `Date.now()` until the cookie expires. 10 | */ 11 | maxAge?: number 12 | 13 | /** 14 | * The URL path on the server on which the cookie will be available. 15 | */ 16 | path?: string 17 | 18 | /** 19 | * The domain that the cookie will be available to. 20 | */ 21 | domain?: string 22 | 23 | /** 24 | * Determine whether the cookie is a 'same-site' cookie. Using `true` translates 25 | * to `'strict'`, using `false` will not set the `sameSite` cookie attribute. 26 | */ 27 | sameSite?: 'strict' | 'lax' | 'none' | true | false 28 | 29 | /** 30 | * Determine whether the cookie is only sent over HTTP(S) 31 | * and not available to client-side JavaScript. 32 | */ 33 | httpOnly?: boolean 34 | 35 | /** 36 | * Determine whether the cookie can only be sent over HTTP (`false`) or HTTPS (`true`). 37 | */ 38 | secure?: boolean 39 | 40 | /** 41 | * Determine whether cookies will be signed using the app key. 42 | */ 43 | signed?: boolean 44 | 45 | /** 46 | * Determine whether to overwrite previously set cookies with the same name. 47 | */ 48 | overwrite?: boolean 49 | } 50 | -------------------------------------------------------------------------------- /packages/contracts/src/http/cors-config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface CorsConfig { 3 | /** 4 | * Controls the `Access-Control-Max-Age` header in seconds. 5 | */ 6 | maxAge?: number 7 | 8 | /** 9 | * `Access-Control-Allow-Methods` 10 | */ 11 | allowedMethods: string | string[] 12 | 13 | /** 14 | * `Access-Control-Allow-Origin`, default is request Origin header. 15 | */ 16 | allowedOrigin?: string 17 | 18 | /** 19 | * `Access-Control-Allow-Headers` 20 | */ 21 | allowedHeaders?: string | string[] 22 | 23 | /** 24 | * `Access-Control-Expose-Headers` 25 | */ 26 | exposedHeaders?: string | string[] 27 | 28 | /** 29 | * `Access-Control-Allow-Credentials` 30 | */ 31 | supportsCredentials?: boolean 32 | } 33 | -------------------------------------------------------------------------------- /packages/contracts/src/http/file-bag.ts: -------------------------------------------------------------------------------- 1 | 2 | import { UploadedFile } from './uploaded-file.js' 3 | 4 | export interface FileBag { 5 | /** 6 | * Returns an object of all uploaded files. 7 | */ 8 | all(...keys: string[]): { [name: string]: UploadedFile | UploadedFile[] | undefined} 9 | 10 | /** 11 | * Returns the uploaded file for the given `name`. 12 | */ 13 | get(name: string): UploadedFile | UploadedFile[] | undefined 14 | 15 | /** 16 | * Add the given file to this header bag. If there’s 17 | */ 18 | add(name: string, file: UploadedFile | UploadedFile[]): this 19 | 20 | /** 21 | * Determine whether a file or list of files exists for the given `name`. 22 | */ 23 | has(name: string): boolean 24 | 25 | /** 26 | * Determine whether files were uploaded on the request. 27 | */ 28 | isEmpty(): boolean 29 | } 30 | -------------------------------------------------------------------------------- /packages/contracts/src/http/methods.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Defines the list of HTTP methods. 4 | */ 5 | export type HttpMethods = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' 6 | -------------------------------------------------------------------------------- /packages/contracts/src/http/middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from '../index.js' 3 | import { HttpContext, NextHandler } from './context.js' 4 | 5 | export type InlineMiddlewareHandler = 6 | (ctx: HttpContext, next: NextHandler) => any | Promise 7 | 8 | export type MiddlewareCtor = new (app: Application) => Middleware 9 | 10 | export interface Middleware { 11 | /** 12 | * Handle the given HTTP `ctx`. 13 | */ 14 | handle(ctx: HttpContext, next: NextHandler): Promise | any 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/src/http/pending-route.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpRoute } from './route.js' 3 | import { RouteHandler } from './router.js' 4 | 5 | export interface PendingRoute { 6 | /** 7 | * Assign a route prefix. 8 | */ 9 | prefix(prefix: string): PendingRoute 10 | 11 | /** 12 | * Assign a middleware to the route. 13 | */ 14 | middleware(middleware: string | string[]): PendingRoute 15 | 16 | /** 17 | * Create a route group 18 | */ 19 | group (callback: () => void): void 20 | 21 | /** 22 | * Create a GET route. 23 | */ 24 | get(path: string, handler: RouteHandler): HttpRoute 25 | 26 | /** 27 | * Create a POST route. 28 | */ 29 | post(path: string, handler: RouteHandler): HttpRoute 30 | 31 | /** 32 | * Create a PUT route. 33 | */ 34 | put(path: string, handler: RouteHandler): HttpRoute 35 | 36 | /** 37 | * Create a DELETE route. 38 | */ 39 | delete(path: string, handler: RouteHandler): HttpRoute 40 | 41 | /** 42 | * Create a PATCH route. 43 | */ 44 | patch(path: string, handler: RouteHandler): HttpRoute 45 | } 46 | -------------------------------------------------------------------------------- /packages/contracts/src/http/query-parameter-bag.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InputBag } from './input-bag.js' 3 | 4 | export interface QueryParameterBag extends InputBag { 5 | /** 6 | * Returns the querystring created from all items in this query parameter bag, 7 | * without the leading question mark `?`. 8 | * 9 | * **Notice:** the returned querystring is encoded. Node.js automatically 10 | * encodes the querystring to ensure a valid URL. Some characters would 11 | * break the URL string otherwise. This way ensures the valid string. 12 | */ 13 | toQuerystring (): string 14 | 15 | /** 16 | * Returns the decoded querystring by running the result of `toQuerystring` 17 | * through `decodeURIComponent`. This method is useful to debug during 18 | * development. It’s recommended to use `toQuerystring` in production. 19 | */ 20 | toQuerystringDecoded (): string 21 | } 22 | -------------------------------------------------------------------------------- /packages/contracts/src/http/redirect.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface HttpRedirect { 3 | /** 4 | * Redirect the request back to the previous path. 5 | */ 6 | back(options?: { fallback: string }): this 7 | 8 | /** 9 | * Redirect the request to the given URL `path`. 10 | */ 11 | to(path: string): this 12 | 13 | /** 14 | * Redirects the request with HTTP status 307. This keeps the request payload 15 | * which is useful for POST/PUT requests containing content. 16 | * 17 | * More details: Details: https://developer.mozilla.org/de/docs/Web/HTTP/Status 18 | */ 19 | withPayload(): this 20 | 21 | /** 22 | * Marks this redirect as permanent with HTTP status 301. 23 | */ 24 | permanent(): this 25 | } 26 | -------------------------------------------------------------------------------- /packages/contracts/src/http/request-headers.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IncomingHttpHeaders } from 'node:http2' 3 | import { RemoveIndexSignature } from '../utils/object.js' 4 | 5 | /** 6 | * This type copies over all properties from the `IncomingHttpHeaders` type 7 | * except the index signature. The index signature is nice to use custom 8 | * HTTP headers, but it throws away IntelliSense which we want to keep. 9 | */ 10 | export type HttpDefaultRequestHeaders = RemoveIndexSignature 11 | 12 | export type HttpDefaultRequestHeader = keyof HttpDefaultRequestHeaders 13 | 14 | /** 15 | * This `HttpRequestHeaders` interface can be used to extend the default 16 | * HTTP headers with custom header key-value pairs. The HTTP request 17 | * picks up the custom headers and keeps IntelliSense for the dev. 18 | * 19 | * You can extend this interface in your code like this: 20 | * 21 | * @example 22 | * 23 | * ```ts 24 | * declare module '@supercharge/contracts' { 25 | * export interface HttpRequestHeaders { 26 | * 'your-header-name': string | undefined 27 | * } 28 | * } 29 | * ``` 30 | */ 31 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 32 | export interface HttpRequestHeaders extends HttpDefaultRequestHeaders { 33 | // 34 | } 35 | 36 | export type HttpRequestHeader = keyof HttpRequestHeaders 37 | -------------------------------------------------------------------------------- /packages/contracts/src/http/route-collection.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpRoute as Route, RouteObjectAttributes } from './route.js' 3 | 4 | export interface HttpRouteCollection { 5 | /** 6 | * Register a new route to the collection. 7 | */ 8 | add (route: Route | Route[]): Route | Route[] 9 | 10 | /** 11 | * Returns all registered routes. 12 | */ 13 | all (): Route[] 14 | 15 | /** 16 | * Clear all registered routes. 17 | */ 18 | clear (): this 19 | 20 | /** 21 | * Returns the number of routes registerd to this route collection. 22 | */ 23 | count (): number 24 | 25 | /** 26 | * Returns the attributes of all routes. 27 | */ 28 | toJSON (): RouteObjectAttributes[] 29 | } 30 | -------------------------------------------------------------------------------- /packages/contracts/src/http/route-group.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface HttpRouteGroup { 3 | /** 4 | * Returns the route group prefix. 5 | */ 6 | prefix(): string 7 | 8 | /** 9 | * Returns the route group middleware stack. 10 | */ 11 | middleware(): string[] 12 | } 13 | -------------------------------------------------------------------------------- /packages/contracts/src/http/route.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpContext } from './context.js' 3 | import { HttpMethods } from './methods.js' 4 | import { RouteHandler } from './router.js' 5 | 6 | export interface RouteObjectAttributes { 7 | path: string 8 | middleware: string[] 9 | methods: HttpMethods[] 10 | isInlineHandler: boolean 11 | isControllerClass: boolean 12 | } 13 | 14 | export interface HttpRoute { 15 | /** 16 | * Assign a route prefix. 17 | */ 18 | prefix(prefix: string): this 19 | 20 | /** 21 | * Assign a middleware to the route. 22 | */ 23 | middleware(middleware: string | string[]): this 24 | 25 | /** 26 | * Returns the route path. 27 | */ 28 | path (): string 29 | 30 | /** 31 | * Returns the route’s HTTP methods. 32 | */ 33 | methods (): HttpMethods[] 34 | 35 | /** 36 | * Returns the route handler. 37 | */ 38 | handler (): RouteHandler 39 | 40 | /** 41 | * Run the route handler. 42 | */ 43 | run (ctx: HttpContext): Promise 44 | 45 | /** 46 | * Returns the route object’s attributes. 47 | */ 48 | toJSON (): RouteObjectAttributes 49 | } 50 | -------------------------------------------------------------------------------- /packages/contracts/src/http/static-assets-config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface StaticAssetsConfig { 3 | /** 4 | * Define the maximum amount of seconds to cache a static resource 5 | * using the `max-age` HTTP header. A cached item is allowed 6 | * to be reused until expired. 7 | */ 8 | maxage: number 9 | 10 | /** 11 | * Determine whether to serve static assets afer running `return next()` 12 | * in the middleware stack. This allows downstream middleware to 13 | * respond first, before serving static assets. 14 | */ 15 | defer: boolean 16 | 17 | /** 18 | * Determine whether to allow serving hidden files from the assets directory. 19 | */ 20 | hidden: boolean 21 | 22 | /** 23 | * Serve an index file, like `index.html` from your static assets. 24 | */ 25 | index: string | false | undefined 26 | } 27 | -------------------------------------------------------------------------------- /packages/contracts/src/http/uploaded-file.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface UploadedFile { 3 | /** 4 | * Returns the file name (according to the uploading client). 5 | */ 6 | name(): string | undefined 7 | 8 | /** 9 | * Returns the current file path. 10 | */ 11 | path(): string 12 | 13 | /** 14 | * Returns the file size in bytes. 15 | */ 16 | size(): number 17 | 18 | /** 19 | * Returns the file’s mime type (according to the uploading client). 20 | */ 21 | mimeType(): string | undefined 22 | 23 | /** 24 | * Return a `Date` instance containing the time this file was last written to. 25 | */ 26 | lastModified(): Date | null | undefined 27 | } 28 | -------------------------------------------------------------------------------- /packages/contracts/src/logging/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface LoggingConfig { 3 | /** 4 | * The logging driver name. 5 | */ 6 | driver: keyof this['channels'] 7 | 8 | /** 9 | * The logging channels config. 10 | */ 11 | channels: LoggingChannels 12 | } 13 | 14 | export interface LoggingChannels { 15 | /** 16 | * Stores the configuration for the file channel. 17 | */ 18 | file?: FileChannelConfig 19 | 20 | /** 21 | * Stores the configuration for the console channel. 22 | */ 23 | console?: ConsoleChannelConfig 24 | } 25 | 26 | export interface ConsoleChannelConfig extends LogChannelConfig {} 27 | 28 | export interface FileChannelConfig extends LogChannelConfig { 29 | /** 30 | * The log file path. 31 | */ 32 | path?: string 33 | } 34 | 35 | export interface LogChannelConfig { 36 | /** 37 | * The minimum logging level. 38 | */ 39 | level?: string 40 | } 41 | -------------------------------------------------------------------------------- /packages/contracts/src/logging/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Logger { 3 | /** 4 | * Log the given `message` at debug level. 5 | */ 6 | debug (message: string, ...context: any[]): void 7 | 8 | /** 9 | * Log the given `message` at info level. 10 | */ 11 | info (message: string, ...context: any[]): void 12 | 13 | /** 14 | * Log the given `message` at trace level. 15 | */ 16 | notice (message: string, ...context: any[]): void 17 | 18 | /** 19 | * Log the given `message` at warn level. 20 | */ 21 | warning (message: string, ...context: any[]): void 22 | 23 | /** 24 | * Log the given `message` at error level. 25 | */ 26 | error (message: string, ...context: any[]): void 27 | 28 | /** 29 | * Log the given `message` at critical level. 30 | */ 31 | critical (message: string, ...context: any[]): void 32 | 33 | /** 34 | * Log the given `message` at alert level. 35 | */ 36 | alert (message: string, ...context: any[]): void 37 | 38 | /** 39 | * Log the given `message` at emergency level. 40 | */ 41 | emergency (message: string, ...context: any[]): void 42 | } 43 | -------------------------------------------------------------------------------- /packages/contracts/src/queue/database-queue.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Job } from './job.js' 3 | 4 | export interface DatabaseQueuePayload { 5 | /** 6 | * The job class name. This job name is used to identify a job class which 7 | * then be used to create a job instance once it’s due for processing will. 8 | */ 9 | jobClassName: string 10 | 11 | /** 12 | * The job payload. 13 | */ 14 | payload: any 15 | 16 | /** 17 | * The queue name on which the job will be dispatched. 18 | */ 19 | queue: string 20 | 21 | /** 22 | * The number of attempts a job has already been handled. 23 | */ 24 | attempts?: number 25 | 26 | /** 27 | * The date when the job becomes due for processing. 28 | */ 29 | notBefore?: Date 30 | } 31 | 32 | export interface DatabaseQueue { 33 | /** 34 | * Push a new job onto the queue. 35 | */ 36 | push (data: DatabaseQueuePayload): Promise 37 | 38 | /** 39 | * Retrieve the next job from the queue. 40 | */ 41 | pop (queue: string): Promise 42 | 43 | /** 44 | * Returns number of jobs on the given `queue`. 45 | */ 46 | size (queue: string): Promise 47 | 48 | /** 49 | * Clear all jobs from the given `queue`. 50 | */ 51 | clear (queue: string): Promise 52 | 53 | /** 54 | * Deletes the job with the given `id` from the queue. 55 | */ 56 | delete (id: string | number): Promise 57 | } 58 | -------------------------------------------------------------------------------- /packages/contracts/src/queue/job.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Job { 3 | /** 4 | * Returns the job ID. 5 | */ 6 | id (): string | number | undefined 7 | 8 | /** 9 | * Returns the queue job class name. 10 | */ 11 | jobName(): string 12 | 13 | /** 14 | * Returns the queue job payload. 15 | */ 16 | payload(): any 17 | 18 | /** 19 | * Returns the number of attempts for this job. 20 | */ 21 | attempts (): number 22 | 23 | /** 24 | * Delete the job from the queue. 25 | */ 26 | delete (): Promise 27 | 28 | /** 29 | * Determine whether a job has been deleted. 30 | */ 31 | isDeleted (): boolean 32 | 33 | /** 34 | * Determine whether a job has not been deleted. 35 | */ 36 | isNotDeleted (): boolean 37 | 38 | /** 39 | * Determine whether a job has been released back onto the queue. 40 | */ 41 | isReleased (): boolean 42 | 43 | /** 44 | * Determine whether a job has not been released back onto the queue. 45 | */ 46 | isNotReleased (): boolean 47 | 48 | /** 49 | * Determine whether a job has been marked as failed. 50 | */ 51 | hasFailed (): boolean 52 | 53 | /** 54 | * Determine whether a job has not been failed. 55 | */ 56 | hasNotFailed (): boolean 57 | 58 | /** 59 | * Set a job as released back to the queue. 60 | */ 61 | releaseBack (delay: number): Promise 62 | 63 | /** 64 | * Mark this job as failed. 65 | */ 66 | markAsFailed (): Promise 67 | 68 | /** 69 | * Fire the job. 70 | */ 71 | fire (): Promise 72 | 73 | /** 74 | * This job ultimately failed. Delete it and call the 75 | * `failed` method if existing on the job instance. 76 | */ 77 | fail (error: Error): Promise 78 | } 79 | -------------------------------------------------------------------------------- /packages/contracts/src/queue/queue.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Job } from './job.js' 3 | 4 | export interface Queue { 5 | /** 6 | * Push a new job onto the queue. 7 | */ 8 | push (jobName: string, payload: any, queue?: string): Promise 9 | 10 | /** 11 | * Retrieve the next job from the queue. 12 | */ 13 | pop (queue?: string): Promise 14 | 15 | /** 16 | * Returns number of jobs on the given `queue`. 17 | */ 18 | size (queue?: string): Promise 19 | 20 | /** 21 | * Clear all jobs from the given `queue`. 22 | */ 23 | clear (queue?: string): Promise 24 | } 25 | -------------------------------------------------------------------------------- /packages/contracts/src/session/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SessionConfig { 3 | /** 4 | * The session driver name. 5 | */ 6 | driver: 'file' | 'memory' | 'cookie' 7 | 8 | /** 9 | * Stores the session cookie name. 10 | */ 11 | name: string 12 | 13 | /** 14 | * The session lifetime in seconds or in a human readable format ('2h'). 15 | */ 16 | lifetime: string | number 17 | 18 | /** 19 | * Clear the session when the browser closes. 20 | */ 21 | expireOnClose?: boolean 22 | 23 | /** 24 | * Determine whether to encrypt the session data. 25 | */ 26 | // encrypt: 27 | 28 | /** 29 | * Stores the session cookie options. 30 | */ 31 | cookie: { 32 | /** 33 | * Stores the session cookie path. 34 | */ 35 | path?: string 36 | 37 | /** 38 | * The domain that the cookie will be available to. 39 | */ 40 | domain?: string 41 | 42 | /** 43 | * Determine whether the session cookie is only to be sent over HTTPS. 44 | */ 45 | secure?: boolean 46 | 47 | /** 48 | * Determine whether the session cookie is only available through the HTTP 49 | * protocol and not from JavaScript. 50 | */ 51 | httpOnly?: boolean 52 | 53 | /** 54 | * Determine whether the session cookie is sent along with cross-site requests. 55 | */ 56 | sameSite?: 'strict' | 'lax' | 'none' | true | false 57 | } 58 | 59 | /** 60 | * Stores the file driver options. 61 | */ 62 | file?: { 63 | location: string 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/contracts/src/session/driver.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SessionDriver { 3 | /** 4 | * Read the session data. 5 | */ 6 | read (sessionId: string): Promise> 7 | 8 | /** 9 | * Store the session data. 10 | */ 11 | write(sessionId: string, values: Record): Promise 12 | 13 | /** 14 | * Delete the session data for the given `sessionId`. 15 | */ 16 | destroy(sessionId: string): Promise 17 | } 18 | -------------------------------------------------------------------------------- /packages/contracts/src/support/htmlable.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Htmlable { 3 | /** 4 | * Returns the content as an HTML string. 5 | */ 6 | toHtml(): string 7 | } 8 | -------------------------------------------------------------------------------- /packages/contracts/src/support/service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from '../index.js' 3 | 4 | type BootCallback = () => Promise | unknown 5 | 6 | export type ServiceProviderCtor = 7 | /** 8 | * Create a new service provider instance. 9 | */ 10 | new(app: Application) => ServiceProvider 11 | 12 | export interface ServiceProvider { 13 | /** 14 | * Returns the application instance. 15 | */ 16 | app (): Application 17 | 18 | /** 19 | * Register application services into the container. 20 | */ 21 | register (app: Application): void 22 | 23 | /** 24 | * Boot application services. 25 | */ 26 | boot? (app: Application): void | Promise 27 | 28 | /** 29 | * Stop application services. 30 | */ 31 | shutdown? (app: Application): void | Promise 32 | 33 | /** 34 | * Register a booting callback that runs before the `boot` method is called. 35 | */ 36 | booting (callback: BootCallback): this 37 | 38 | /** 39 | * Register a booted callback that runs after the `boot` method was called. 40 | */ 41 | booted (callback: BootCallback): this 42 | 43 | /** 44 | * Call the registered booting callbacks. 45 | */ 46 | callBootingCallbacks (): Promise 47 | 48 | /** 49 | * Call the registered booted callbacks. 50 | */ 51 | callBootedCallbacks (): Promise 52 | 53 | /** 54 | * Merge the content of the configuration file located at the 55 | * given `filePath` with the existing app configuration. 56 | */ 57 | mergeConfigFrom (filePath: string, key: string): Promise 58 | } 59 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/class.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Class = new(...arguments_: Arguments) => T 3 | -------------------------------------------------------------------------------- /packages/contracts/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Dict { 3 | [key: string]: T | undefined 4 | } 5 | 6 | export type RemoveIndexSignature = { 7 | [K in keyof T as string extends K 8 | ? never 9 | : number extends K 10 | ? never 11 | : symbol extends K 12 | ? never 13 | : K 14 | ]: T[K]; 15 | } 16 | -------------------------------------------------------------------------------- /packages/contracts/src/view/config-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViewConfigBuilder { 3 | /** 4 | * Set the base layout used to render this view. The given `name` identifies 5 | * the file name of the layout file in the configured layouts folder. 6 | */ 7 | layout(name: string): this 8 | 9 | /** 10 | * Render this view without a base layout. 11 | */ 12 | withoutLayout(): this 13 | } 14 | -------------------------------------------------------------------------------- /packages/contracts/src/view/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViewConfig { 3 | /** 4 | * Defines the default view driver. 5 | */ 6 | driver: 'handlebars' | string 7 | 8 | /** 9 | * The Handlebars view config. 10 | */ 11 | handlebars: { 12 | /** 13 | * Stores the path to the view files. 14 | */ 15 | views: string 16 | 17 | /** 18 | * Stores the path to the view partial files. 19 | */ 20 | partials: string 21 | 22 | /** 23 | * Stores the path to the view helper files. 24 | */ 25 | helpers: string 26 | 27 | /** 28 | * Stores the path to the view layout files. 29 | */ 30 | layouts: string 31 | 32 | /** 33 | * Stores the name of the default layout. 34 | */ 35 | defaultLayout: string 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/contracts/src/view/response-config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViewResponseConfig { 3 | /** 4 | * Defines the base layout used to render a view. 5 | */ 6 | layout?: string 7 | } 8 | -------------------------------------------------------------------------------- /packages/contracts/src/view/response.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ViewConfigBuilder } from './config-builder.js' 3 | 4 | export type ViewBuilderCallback = (viewBuilder: ViewConfigBuilder) => unknown 5 | -------------------------------------------------------------------------------- /packages/contracts/src/vite/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViteConfig { 3 | /** 4 | * Stores the URL used as a prefix when creating asset URLs. This could be a 5 | * CDN URL for production builds. If empty, the created asset URL starts 6 | * with a leading slash to serve it locally from the running server. 7 | * 8 | * @default `/build` 9 | */ 10 | assetsUrl?: string 11 | 12 | /** 13 | * Stores the path to the hot-reload file, relative from the application’s base directory. 14 | * 15 | * @default `/.vite/hot.json` 16 | */ 17 | hotReloadFilePath?: string 18 | 19 | /** 20 | * Stores the Vite manifest file path, relative from the application’s base directory. 21 | * 22 | * @default `/.vite/manifest.json` 23 | */ 24 | manifestFilePath?: string 25 | 26 | /** 27 | * Stores an object of attributes to apply on all HTML `script` tags. 28 | * 29 | * @default `{}` 30 | */ 31 | scriptAttributes?: Record 32 | 33 | /** 34 | * Stores an object of attributes to apply on all HTML `style` tags. 35 | * 36 | * @default `{}` 37 | */ 38 | styleAttributes?: Record 39 | } 40 | -------------------------------------------------------------------------------- /packages/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "emitDeclarationOnly": true 7 | }, 8 | "exclude": [ 9 | "dist", 10 | "node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/application.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpServiceProvider } from '@supercharge/http' 3 | import { Application as BaseApplication } from '@supercharge/application' 4 | 5 | export class Application extends BaseApplication { 6 | /** 7 | * Create a new application instance. 8 | */ 9 | constructor (basePath: string) { 10 | super(basePath) 11 | 12 | this.registerCoreServiceProviders() 13 | } 14 | 15 | /** 16 | * Register the base service provider into the container. 17 | */ 18 | private registerCoreServiceProviders (): void { 19 | this 20 | .register(new HttpServiceProvider(this)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/bootstrappers/boot-service-providers.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application, Bootstrapper } from '@supercharge/contracts' 3 | 4 | export class BootServiceProviders implements Bootstrapper { 5 | /** 6 | * Bootstrap the given application. 7 | */ 8 | async bootstrap (app: Application): Promise { 9 | await app.boot() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/bootstrappers/handle-exceptions.ts: -------------------------------------------------------------------------------- 1 | 2 | import Youch from 'youch' 3 | // @ts-expect-error 4 | import toTerminal from 'youch-terminal' 5 | import { tap } from '@supercharge/goodies' 6 | import { Bootstrapper } from '@supercharge/contracts' 7 | 8 | export class HandleExceptions implements Bootstrapper { 9 | /** 10 | * Bootstrap the application. This will listen for exceptions 11 | * crashing your Node.js process. The listeners will print 12 | * a message to the terminal before exiting the process. 13 | */ 14 | async bootstrap (): Promise { 15 | process 16 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 17 | .on('uncaughtException', async (error: Error) => { 18 | await this.handle(error) 19 | }) 20 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 21 | .on('unhandledRejection', async (error: Error) => { 22 | await this.handle(error) 23 | }) 24 | } 25 | 26 | /** 27 | * Pretty-print the given `error` to the terminal. 28 | */ 29 | async handle (error: Error): Promise { 30 | await tap(new Youch(error, {}).toJSON(), output => { 31 | console.log(toTerminal(output)) 32 | process.exit(1) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/bootstrappers/handle-shutdown.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application, Bootstrapper } from '@supercharge/contracts' 3 | import { ShutdownSignalListener } from '../shutdown-signal-listener.js' 4 | 5 | export class HandleShutdown implements Bootstrapper { 6 | private readonly app: Application 7 | private readonly shutdownSignalListener: ShutdownSignalListener 8 | 9 | /** 10 | * Create a new instance. 11 | */ 12 | constructor (app: Application) { 13 | this.app = app 14 | this.shutdownSignalListener = new ShutdownSignalListener(app) 15 | } 16 | 17 | /** 18 | * Register a listener for shutdown signals (`SIGINT` and `SIGTERM` by default). 19 | */ 20 | async bootstrap (): Promise { 21 | this.shutdownSignalListener 22 | .onShutdown(async () => await this.app.shutdown()) 23 | .onShutdown(() => this.shutdownSignalListener.cleanup()) 24 | .listen() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/bootstrappers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './handle-exceptions.js' 3 | export * from './handle-shutdown.js' 4 | export * from './load-configuration.js' 5 | export * from './load-environment-variables.js' 6 | export * from './boot-service-providers.js' 7 | export * from './register-service-providers.js' 8 | -------------------------------------------------------------------------------- /packages/core/src/bootstrappers/register-service-providers.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application, Bootstrapper } from '@supercharge/contracts' 3 | 4 | export class RegisterServiceProviders implements Bootstrapper { 5 | /** 6 | * Bootstrap the given application. 7 | */ 8 | async bootstrap (app: Application): Promise { 9 | await app.registerConfiguredProviders() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/errors/environment-file-error.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Error } from '@supercharge/errors' 3 | 4 | export class EnvironmentFileError extends Error { 5 | /** 6 | * Create a new error instance. 7 | */ 8 | constructor (message: string, cause?: any) { 9 | super(message, { cause }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/errors/http-error.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpError as BaseHttpError } from '@supercharge/errors' 3 | import { HttpContext, RenderableError, ReportableError } from '@supercharge/contracts' 4 | 5 | export class HttpError extends BaseHttpError implements RenderableError, ReportableError { 6 | /** 7 | * Create a new HTTP error instance. 8 | */ 9 | constructor (message: string, cause?: any) { 10 | super(message, { cause }) 11 | 12 | this.withStatus(500) 13 | } 14 | 15 | /** 16 | * Returns a new HTTP error instance wrapping the given `error`. 17 | */ 18 | static wrap (error: Error): HttpError { 19 | const err = new this(error.message, error).withStatus( 20 | this.retrieveStatusFrom(error) 21 | ) 22 | 23 | if (error.stack) { 24 | err.withStack(error.stack) 25 | } 26 | 27 | return err 28 | } 29 | 30 | /** 31 | * Retrieves an available status code from the error instance. 32 | * Falls back to HTTP status 500 if no status code is found. 33 | */ 34 | private static retrieveStatusFrom (error: any): number { 35 | return error.status || error.statusCode || 500 36 | } 37 | 38 | /** 39 | * Report the given `error` on the related HTTP `ctx`. Return a `falsy` 40 | * value if you don’t want the Supercharge error handler to run the 41 | * registered reporters and logging for the given error instance. 42 | */ 43 | report (_error: any, _ctx: HttpContext): Promise | any { 44 | // 45 | } 46 | 47 | /** 48 | * Render the given `error` on the related HTTP `ctx`. Return a `falsy` 49 | * value if you don’t want the Supercharge error handler to render 50 | * the error instance into an HTTP response, like view or JSON. 51 | */ 52 | render (_error: any, _ctx: HttpContext): Promise | any { 53 | // 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './handler.js' 3 | export * from './http-error.js' 4 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Application } from './application.js' 3 | 4 | export { 5 | BootServiceProviders, 6 | HandleExceptions, 7 | HandleShutdown, 8 | LoadConfiguration, 9 | LoadEnvironmentVariables, 10 | RegisterServiceProviders 11 | } from './bootstrappers/index.js' 12 | 13 | export { ConsoleKernel } from './console/kernel.js' 14 | export { ErrorHandler, HttpError } from './errors/index.js' 15 | export { HttpKernel } from './http/kernel.js' 16 | export { RouteServiceProvider } from './providers/index.js' 17 | export { Command } from '@supercharge/console' 18 | -------------------------------------------------------------------------------- /packages/core/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './route-service-provider.js' 3 | -------------------------------------------------------------------------------- /packages/core/src/providers/route-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { tap } from '@supercharge/goodies' 3 | import { ServiceProvider } from '@supercharge/support' 4 | 5 | export class RouteServiceProvider extends ServiceProvider { 6 | /** 7 | * Stores the callback functions that load route files. 8 | */ 9 | private readonly loadRoutesCallbacks: Array<(() => void)> = [] 10 | 11 | /** 12 | * Register application services to the container. 13 | */ 14 | override register (): void { 15 | this.booted(async () => { 16 | await this.runRouteLoadingCallbacks() 17 | }) 18 | } 19 | 20 | /** 21 | * Run the registered route-loading callbacks. 22 | */ 23 | protected async runRouteLoadingCallbacks (): Promise { 24 | for (const callback of this.loadRoutesCallbacks) { 25 | await callback() 26 | } 27 | } 28 | 29 | /** 30 | * Load the routes file from the given `path`. 31 | */ 32 | loadRoutesFrom (path: string): this { 33 | return this.loadRoutesUsing(async () => { 34 | await import(`${path}?imported=${Date.now()}`) 35 | }) 36 | } 37 | 38 | /** 39 | * Register a callback that will be used to load the application’s routes. 40 | */ 41 | loadRoutesUsing (callback: () => Promise | unknown): this { 42 | return tap(this, () => { 43 | this.loadRoutesCallbacks.push(callback) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | OVERWRITE=0 3 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/.env.testing: -------------------------------------------------------------------------------- 1 | TESTING=set-when-loading-env-testing 2 | OVERWRITE=1 3 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/app/console/commands/test-command.js: -------------------------------------------------------------------------------- 1 | 2 | import { Command } from '@supercharge/console' 3 | 4 | export default class TestCommand extends Command { 5 | configure () { 6 | this.name('test:command') 7 | } 8 | 9 | run () { 10 | console.log('running test:command') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/bootstrap/providers.js: -------------------------------------------------------------------------------- 1 | 2 | export const providers = [] 3 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/config/ignored.txt: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | foo: 'bar' 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/config/test.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercharge/framework/21992528dd0ba30b3a1b505e14125d77b7d365f6/packages/core/test/fixtures/config/test.d.ts -------------------------------------------------------------------------------- /packages/core/test/fixtures/config/test.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | foo: 'bar' 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core-test-fixtures", 3 | "version": "1.2.3", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/resources/views/errors/401.hbs: -------------------------------------------------------------------------------- 1 |

error-view

2 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/routes/web.js: -------------------------------------------------------------------------------- 1 | 2 | globalThis.valueSetByRouteServiceProvider = 'Supercharge' 3 | -------------------------------------------------------------------------------- /packages/core/test/route-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Path from 'node:path' 4 | import { expect } from 'expect' 5 | import { fileURLToPath } from 'node:url' 6 | import { HttpKernel, Application, ErrorHandler, RouteServiceProvider as BaseRouteServiceProvider } from '../dist/index.js' 7 | 8 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 9 | const appRootPath = Path.resolve(__dirname, './fixtures') 10 | 11 | class RouteServiceProvider extends BaseRouteServiceProvider { 12 | /** 13 | * Boot the service provider. 14 | */ 15 | async boot () { 16 | this.loadRoutesFrom( 17 | this.app().resolveGlobFromBasePath('routes/web.**') 18 | ) 19 | } 20 | } 21 | 22 | let app = createApp() 23 | 24 | function createApp () { 25 | const app = Application 26 | .createWithAppRoot(appRootPath) 27 | .withErrorHandler(ErrorHandler) 28 | .bind('view', () => { 29 | // empty view mock 30 | }) 31 | 32 | app.register( 33 | new RouteServiceProvider(app) 34 | ) 35 | 36 | app.config().set('app.key', 1234) 37 | app.config().set('http', { 38 | host: 'localhost', 39 | port: 1234, 40 | cookie: {} 41 | }) 42 | 43 | return app 44 | } 45 | 46 | test.before.each(() => { 47 | app = createApp() 48 | }) 49 | 50 | test('boot route service provider', async () => { 51 | const kernel = new HttpKernel(app) 52 | await kernel.prepare() 53 | expect(globalThis.valueSetByRouteServiceProvider).toEqual('Supercharge') 54 | }) 55 | 56 | test.run() 57 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../application" 17 | }, 18 | { 19 | "path": "../console" 20 | }, 21 | { 22 | "path": "../contracts" 23 | }, 24 | { 25 | "path": "../http" 26 | }, 27 | { 28 | "path": "../support" 29 | }, 30 | { 31 | "path": "../view" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/database", 3 | "description": "The Supercharge database package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "type": "module", 10 | "main": "dist/index.js", 11 | "types": "dist", 12 | "exports": { 13 | ".": "./dist/index.js" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/manager": "^4.0.0-alpha.2", 21 | "@supercharge/support": "^4.0.0-alpha.2", 22 | "knex": "~3.1.0", 23 | "objection": "~3.1.3", 24 | "sqlite3": "~5.1.6" 25 | }, 26 | "devDependencies": { 27 | "@supercharge/core": "^4.0.0-alpha.2", 28 | "c8": "~9.1.0", 29 | "expect": "~29.7.0", 30 | "typescript": "~5.4.5", 31 | "uvu": "~0.5.6" 32 | }, 33 | "engines": { 34 | "node": ">=22" 35 | }, 36 | "homepage": "https://superchargejs.com", 37 | "keywords": [ 38 | "supercharge", 39 | "superchargejs", 40 | "nodejs", 41 | "database", 42 | "orm", 43 | "knex", 44 | "objection" 45 | ], 46 | "license": "MIT", 47 | "publishConfig": { 48 | "access": "public" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "directory": "packages/database", 53 | "url": "git+https://github.com/supercharge/framework.git" 54 | }, 55 | "scripts": { 56 | "build": "tsc --build tsconfig.json --force", 57 | "dev": "npm run build -- --watch", 58 | "watch": "npm run dev", 59 | "lint": "eslint src --ext .js,.ts", 60 | "lint:fix": "npm run lint -- --fix", 61 | "test": "npm run build && npm run lint && npm run test:run", 62 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 63 | "posttest": "c8 report --reporter=html" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/database/src/database-manager-proxy.ts: -------------------------------------------------------------------------------- 1 | 2 | export class DatabaseManagerProxy { 3 | /** 4 | * The class instance this proxy handler is applied to. 5 | */ 6 | private readonly class: T 7 | 8 | /** 9 | * The method name to call for missing methods. 10 | */ 11 | private readonly method: string = '__call' 12 | 13 | /** 14 | * Create a new call through handler for the given `Class`. 15 | */ 16 | constructor (Class: T) { 17 | this.class = Class 18 | } 19 | 20 | /** 21 | * The trap for getting values on the given `target`. 22 | */ 23 | get (target: any, property: string): R | Function { 24 | if (Reflect.has(target, property)) { 25 | return Reflect.get(target, property) 26 | } 27 | 28 | const bind = target[this.method].call(this.class, property) 29 | 30 | if (typeof bind[property] === 'function') { 31 | return (...args: any[]) => { 32 | return bind[property](...args) 33 | } 34 | } 35 | 36 | return bind[property] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/database/src/database-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Model } from './model.js' 3 | import { DatabaseManager } from './database-manager.js' 4 | import { ServiceProvider } from '@supercharge/support' 5 | import { DatabaseConfig } from '@supercharge/contracts' 6 | 7 | /** 8 | * Add container bindings for services from this provider. 9 | */ 10 | declare module '@supercharge/contracts' { 11 | export interface ContainerBindings { 12 | 'db': DatabaseManager 13 | } 14 | } 15 | 16 | export class DatabaseServiceProvider extends ServiceProvider { 17 | /** 18 | * Register application services into the container. 19 | */ 20 | override register (): void { 21 | this.app().singleton('db', () => { 22 | const databaseConfig = this.app().config().get('database') 23 | 24 | return new DatabaseManager(databaseConfig) 25 | }) 26 | 27 | Model.knex( 28 | this.app().make('db').connection() 29 | ) 30 | } 31 | 32 | /** 33 | * Stop application services. 34 | */ 35 | override async shutdown (): Promise { 36 | for (const connection of this.app().make('db').connections()) { 37 | await connection.destroy() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/database/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { DatabaseManager } from './database-manager.js' 3 | export { DatabaseServiceProvider } from './database-service-provider.js' 4 | export { Model } from './model.js' 5 | export { QueryBuilder } from './query-builder.js' 6 | -------------------------------------------------------------------------------- /packages/database/src/model.ts: -------------------------------------------------------------------------------- 1 | 2 | import { QueryBuilder } from './query-builder.js' 3 | import { Constructor, MaybeCompositeId, MaybeSingleQueryBuilder, Model as BaseModel, NumberQueryBuilder, QueryBuilderType, TransactionOrKnex } from 'objection' 4 | 5 | export class Model extends BaseModel { 6 | /** 7 | * Both of these type definitions are needed to make sure that every model 8 | * inheriting from this base class has the fluent query builder available. 9 | */ 10 | declare QueryBuilderType: QueryBuilder 11 | static override QueryBuilder = QueryBuilder 12 | 13 | /** 14 | * Find an item of this model for the given `id`. 15 | */ 16 | static findById (this: Constructor, id: MaybeCompositeId, trx?: TransactionOrKnex): MaybeSingleQueryBuilder> { 17 | return (this as unknown as typeof Model).query(trx).findById(id) 18 | } 19 | 20 | /** 21 | * Delete an item of this model for the given `id`. 22 | */ 23 | static deleteById (this: Constructor, id: MaybeCompositeId, trx?: TransactionOrKnex): NumberQueryBuilder> { 24 | return (this as unknown as typeof Model).query(trx).deleteById(id) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/database/src/query-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Model, Page, QueryBuilder as BaseQueryBuilder } from 'objection' 3 | 4 | export class QueryBuilder extends BaseQueryBuilder { 5 | /** 6 | * The following properties are necessary to have proper TypeScript support. 7 | */ 8 | declare ArrayQueryBuilderType: QueryBuilder 9 | declare SingleQueryBuilderType: QueryBuilder 10 | declare MaybeSingleQueryBuilderType: QueryBuilder 11 | declare NumberQueryBuilderType: QueryBuilder 12 | declare PageQueryBuilderType: QueryBuilder> 13 | 14 | /** 15 | * Fails the query if the result set is empty. Use the given `callback` to 16 | * throw a custom error. Otherwise, this method throws a generic `Error`. 17 | */ 18 | public orFail (callback?: () => any): QueryBuilder['SingleQueryBuilderType'] { 19 | return this.runAfter((result: any) => { 20 | if (this.hasResults(result)) { 21 | return result 22 | } 23 | 24 | if (typeof callback === 'function') { 25 | callback() 26 | } 27 | 28 | throw new Error(`Failed to find instance for "${this.modelClass().name}"`) 29 | }) as any 30 | } 31 | 32 | /** 33 | * Determine whether the given `result` contains items in the query result. 34 | */ 35 | protected hasResults (result: any): boolean { 36 | if (Array.isArray(result)) { 37 | return result.length > 0 38 | } 39 | 40 | return !!result 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/database/test/database-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { makeApp } from './helpers/index.js' 5 | import { DatabaseServiceProvider, DatabaseManager } from '../dist/index.js' 6 | 7 | test('registers DB service provider', async t => { 8 | const app = makeApp() 9 | app.register(new DatabaseServiceProvider(app)) 10 | 11 | expect(app.make('db')).not.toBeNull() 12 | expect(app.make('db')).not.toBeUndefined() 13 | expect(app.make('db')).toBeInstanceOf(DatabaseManager) 14 | }) 15 | 16 | test.run() 17 | -------------------------------------------------------------------------------- /packages/database/test/database.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { DatabaseManager } from '../dist/index.js' 5 | import { makeDb, makeApp } from './helpers/index.js' 6 | 7 | test('throws for missing connection name', async t => { 8 | const app = makeApp({ 9 | connection: 'unavailable', 10 | connections: { mysql: {} } 11 | }) 12 | 13 | const dbConfig = app.config().get('database') 14 | 15 | expect(() => { 16 | new DatabaseManager(dbConfig).connection() 17 | }).toThrow('Database connection "unavailable" is not configured') 18 | 19 | expect(() => { 20 | new DatabaseManager(dbConfig).connection('unavailable') 21 | }).toThrow('Database connection "unavailable" is not configured') 22 | 23 | expect(() => { 24 | new DatabaseManager(dbConfig).isMissingConnection() 25 | }).toThrow() 26 | }) 27 | 28 | test('throws when not providing a connection while checking for connections', async t => { 29 | expect(() => { 30 | new DatabaseManager({}).isMissingConnection() 31 | }).toThrow('You must provide a database connection "name"') 32 | }) 33 | 34 | test('connects to the database', async t => { 35 | const db = makeDb() 36 | const tableName = 'users' 37 | 38 | if (!await db.schema.hasTable(tableName)) { 39 | await db.schema.createTable(tableName, table => { 40 | table.string('name') 41 | }) 42 | } 43 | 44 | await db.transaction(async trx => { 45 | await trx(tableName).insert({ name: 'Marcus' }) 46 | }) 47 | 48 | if (await db.schema.hasTable(tableName)) { 49 | await db.schema.dropTable(tableName) 50 | } 51 | 52 | await db.destroy() 53 | }) 54 | 55 | test('fails to connect to the database', async () => { 56 | expect(() => { 57 | return new DatabaseManager({ connections: {} }).connection('postgres') 58 | }).toThrow('Database connection "postgres" is not configured') 59 | }) 60 | 61 | test.run() 62 | -------------------------------------------------------------------------------- /packages/database/test/helpers/post-model.js: -------------------------------------------------------------------------------- 1 | 2 | import UserModel from './user-model.js' 3 | import { Model } from '../../dist/index.js' 4 | 5 | export default class PostModel extends Model { 6 | static get tableName () { 7 | return 'posts' 8 | } 9 | 10 | static get jsonSchema () { 11 | return { 12 | type: 'object', 13 | 14 | properties: { 15 | id: { type: 'number' }, 16 | title: { type: 'string' }, 17 | userId: { type: 'integer', relation: 'user' } 18 | } 19 | } 20 | } 21 | 22 | static get relationMappings () { 23 | return { 24 | user: { 25 | relation: Model.BelongsToOneRelation, 26 | modelClass: UserModel, 27 | join: { from: 'posts.userId', to: `${UserModel.tableName}.id` } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/database/test/helpers/user-model.js: -------------------------------------------------------------------------------- 1 | 2 | import PostModel from './post-model.js' 3 | import { Model } from '../../dist/index.js' 4 | 5 | export default class UserModel extends Model { 6 | static get tableName () { 7 | return 'users_table' 8 | } 9 | 10 | static get jsonSchema () { 11 | return { 12 | type: 'object', 13 | 14 | properties: { 15 | id: { type: 'number' }, 16 | name: { type: 'string' } 17 | } 18 | } 19 | } 20 | 21 | static get relationMappings () { 22 | return { 23 | posts: { 24 | relation: Model.HasManyRelation, 25 | modelClass: PostModel, 26 | join: { from: `${this.tableName}.id`, to: 'posts.userId' } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/database/test/model.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @typedef {import('@supercharge/contracts').Database } Database 4 | */ 5 | 6 | import { test } from 'uvu' 7 | import { expect } from 'expect' 8 | import UserModel from './helpers/user-model.js' 9 | import { DatabaseServiceProvider } from '../dist/index.js' 10 | import { makeApp, clearDbDirectory } from './helpers/index.js' 11 | 12 | let app = makeApp() 13 | 14 | test.before(async () => { 15 | app = makeApp() 16 | 17 | app.register(new DatabaseServiceProvider(app)) 18 | 19 | const db = app.make('db') 20 | const tableName = UserModel.tableName 21 | 22 | if (await db.schema.hasTable(tableName)) { 23 | await db.schema.dropTable(tableName) 24 | } 25 | 26 | await db.schema.createTable(tableName, table => { 27 | table.increments('id') 28 | table.string('name') 29 | }) 30 | }) 31 | 32 | test.after(async () => { 33 | const db = app.make('db') 34 | 35 | if (await db.schema.hasTable(UserModel.tableName)) { 36 | await db.schema.dropTable(UserModel.tableName) 37 | } 38 | 39 | await db.destroy() 40 | 41 | await clearDbDirectory() 42 | }) 43 | 44 | test('findById', async t => { 45 | await UserModel.query().insert({ name: 'Supercharge' }) 46 | 47 | const user = await UserModel.findById(1) 48 | expect(user).toEqual({ id: 1, name: 'Supercharge' }) 49 | }) 50 | 51 | test('finds when findByIdOrFail', async t => { 52 | const user = await UserModel.query().insert({ name: 'Supercharge' }) 53 | 54 | expect( 55 | await UserModel.findById(user.id).orFail() 56 | ).toEqual(user) 57 | }) 58 | 59 | test('deleteById', async t => { 60 | const user = await UserModel.query().insert({ name: 'Supercharge' }) 61 | 62 | const deleted = await UserModel.deleteById(user.id) 63 | expect(deleted).toBe(1) 64 | 65 | const deletedAgain = await UserModel.deleteById(user.id) 66 | expect(deletedAgain).toBe(0) 67 | }) 68 | 69 | test.run() 70 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | }, 18 | { 19 | "path": "../manager" 20 | }, 21 | { 22 | "path": "../support" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/encryption/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/encryption", 3 | "description": "The Supercharge encryption package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/json": "~2.0.0", 21 | "@supercharge/strings": "~2.0.0", 22 | "@supercharge/support": "^4.0.0-alpha.2" 23 | }, 24 | "devDependencies": { 25 | "@supercharge/application": "^4.0.0-alpha.2", 26 | "c8": "~9.1.0", 27 | "expect": "~29.7.0", 28 | "typescript": "~5.4.5", 29 | "uvu": "~0.5.6" 30 | }, 31 | "engines": { 32 | "node": ">=22" 33 | }, 34 | "homepage": "https://superchargejs.com", 35 | "keywords": [ 36 | "encryption", 37 | "aes", 38 | "supercharge", 39 | "superchargejs", 40 | "nodejs" 41 | ], 42 | "license": "MIT", 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "directory": "packages/encryption", 49 | "url": "git+https://github.com/supercharge/framework.git" 50 | }, 51 | "scripts": { 52 | "build": "tsc --build tsconfig.json --force", 53 | "watch": "npm run build -- --watch", 54 | "lint": "eslint src --ext .js,.ts", 55 | "lint:fix": "npm run lint -- --fix", 56 | "test": "npm run build && npm run lint && npm run test:run", 57 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 58 | "posttest": "c8 report --reporter=html" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/encryption/src/encryption-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Encrypter } from './encrypter.js' 3 | import { ServiceProvider } from '@supercharge/support' 4 | 5 | /** 6 | * Add container bindings for services from this provider. 7 | */ 8 | declare module '@supercharge/contracts' { 9 | export interface ContainerBindings { 10 | 'encrypter': Encrypter 11 | 'encryption': ContainerBindings['encrypter'] 12 | } 13 | } 14 | 15 | export class EncryptionServiceProvider extends ServiceProvider { 16 | /** 17 | * Register the encrypter into the container. 18 | */ 19 | override register (): void { 20 | this.registerEncrypter() 21 | } 22 | 23 | /** 24 | * Register the encrypter instance. 25 | */ 26 | protected registerEncrypter (): void { 27 | this.app() 28 | .singleton('encrypter', () => { 29 | return new Encrypter({ key: this.config().get('app.key') }) 30 | }) 31 | .alias('encrypter', 'encryption') 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/encryption/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Encrypter, EncrypterOptions } from './encrypter.js' 3 | export { EncryptionServiceProvider } from './encryption-service-provider.js' 4 | -------------------------------------------------------------------------------- /packages/encryption/test/encryption-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Path from 'node:path' 4 | import { expect } from 'expect' 5 | import { fileURLToPath } from 'node:url' 6 | import { Str } from '@supercharge/strings' 7 | import { Application } from '@supercharge/application' 8 | import { Encrypter, EncryptionServiceProvider } from '../dist/index.js' 9 | 10 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 11 | const fixturesPath = Path.resolve(__dirname, './fixtures') 12 | 13 | const app = Application.createWithAppRoot(fixturesPath) 14 | 15 | app.config().set('app.key', Str.random(32)) 16 | 17 | test('register encrypter and aliases', async () => { 18 | app.register(new EncryptionServiceProvider(app)) 19 | 20 | expect(app.make('encrypter')).toBeInstanceOf(Encrypter) 21 | expect(app.make('encryption')).toBeInstanceOf(Encrypter) 22 | }) 23 | 24 | test.run() 25 | -------------------------------------------------------------------------------- /packages/encryption/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../application" 17 | }, 18 | { 19 | "path": "../contracts" 20 | }, 21 | { 22 | "path": "../support" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/env/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/env", 3 | "description": "The Supercharge environment package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/fs": "~3.4.0", 21 | "@supercharge/strings": "~2.0.0", 22 | "dotenv": "~16.3.1" 23 | }, 24 | "devDependencies": { 25 | "@types/lodash": "~4.14.202", 26 | "c8": "~9.1.0", 27 | "expect": "~29.7.0", 28 | "typescript": "~5.4.5", 29 | "uvu": "~0.5.6" 30 | }, 31 | "engines": { 32 | "node": ">=16" 33 | }, 34 | "homepage": "https://superchargejs.com", 35 | "keywords": [ 36 | "env", 37 | "environment", 38 | "supercharge", 39 | "superchargejs", 40 | "nodejs" 41 | ], 42 | "license": "MIT", 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "directory": "packages/env", 49 | "url": "git+https://github.com/supercharge/framework.git" 50 | }, 51 | "scripts": { 52 | "build": "tsc --build tsconfig.json --force", 53 | "dev": "npm run build -- --watch", 54 | "watch": "npm run dev", 55 | "lint": "eslint src --ext .js,.ts", 56 | "lint:fix": "npm run lint -- --fix", 57 | "test": "npm run build && npm run lint && npm run test:run", 58 | "test:run": "NODE_ENV=testing c8 --include=dist uvu --ignore helpers --ignore fixtures", 59 | "posttest": "c8 report --reporter=html" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/env/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Env } from './env.js' 3 | -------------------------------------------------------------------------------- /packages/env/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/facades/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/facades", 3 | "description": "The Supercharge facades package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/method-missing": "~1.0.0" 21 | }, 22 | "devDependencies": { 23 | "c8": "~9.1.0", 24 | "expect": "~29.7.0", 25 | "typescript": "~5.4.5", 26 | "uvu": "~0.5.6" 27 | }, 28 | "engines": { 29 | "node": ">=22" 30 | }, 31 | "homepage": "https://superchargejs.com", 32 | "keywords": [ 33 | "supercharge", 34 | "superchargejs", 35 | "facade", 36 | "facades", 37 | "nodejs" 38 | ], 39 | "license": "MIT", 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "directory": "packages/facades", 46 | "url": "git+https://github.com/supercharge/framework.git" 47 | }, 48 | "scripts": { 49 | "build": "tsc --build tsconfig.json --force", 50 | "dev": "npm run build -- --watch", 51 | "watch": "npm run dev", 52 | "lint": "eslint src --ext .js,.ts", 53 | "lint:fix": "npm run lint -- --fix", 54 | "test": "npm run build && npm run lint && npm run test:run", 55 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 56 | "posttest": "c8 report --reporter=html" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/facades/src/app.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class AppFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'app' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class ConfigFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'config' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/crypt.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class CryptFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'encrypter' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/database.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class DatabaseFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'db' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/env.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class EnvFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'env' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/facade.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application } from '@supercharge/contracts' 3 | import MethodMissing from '@supercharge/method-missing' 4 | 5 | export class Facade extends MethodMissing { 6 | /** 7 | * The application instance 8 | */ 9 | static app: Application 10 | 11 | /** 12 | * Set the application instance. 13 | */ 14 | static setApplication (app: Application): typeof Facade { 15 | this.app = app 16 | 17 | return this 18 | } 19 | 20 | /** 21 | * Returns the container binding name. 22 | */ 23 | getContainerNamespace (): string { 24 | throw new Error(`The facade ${this.constructor.name} must implement the getContainerNamespace method.`) 25 | } 26 | 27 | /** 28 | * Returns the facade instance resolved from the IoC container. 29 | */ 30 | resolveFacadeInstance (namespace: string): unknown { 31 | return Facade.app.make(namespace) 32 | } 33 | 34 | /** 35 | * Returns the facade instance. 36 | */ 37 | getFacadeInstance (): any { 38 | const facade = this.resolveFacadeInstance( 39 | this.getContainerNamespace() 40 | ) 41 | 42 | if (!facade) { 43 | throw new Error(`Failed to retrieve facade instance for binding "${this.getContainerNamespace()}"`) 44 | } 45 | 46 | return facade 47 | } 48 | 49 | /** 50 | * Pass through all calls to the facaded instance. 51 | */ 52 | __call (methodName: string, args: unknown[]): unknown { 53 | if (this.getFacadeInstance()[methodName]) { 54 | return this.getFacadeInstance()[methodName](...args) 55 | } 56 | 57 | throw new Error(`Missing method "${methodName}" on facade ${this.getContainerNamespace()}`) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/facades/src/hash.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class HashFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'hash' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AppFacade } from './app.js' 3 | import { EnvFacade } from './env.js' 4 | import { HashFacade } from './hash.js' 5 | import { ViewFacade } from './view.js' 6 | import { LogFacade } from './logger.js' 7 | import { CryptFacade } from './crypt.js' 8 | import { RouteFacade } from './route.js' 9 | import { ConfigFacade } from './config.js' 10 | import { DatabaseFacade } from './database.js' 11 | import { Application, ConfigStore, Database as DatabaseContract, Encrypter, EnvStore, Logger, HttpRouter, ViewEngine, Hasher } from '@supercharge/contracts' 12 | 13 | const Log: Logger = new LogFacade() as unknown as Logger 14 | const Hash: Hasher = new HashFacade() as unknown as Hasher 15 | const Env: EnvStore = new EnvFacade() as unknown as EnvStore 16 | const App: Application = new AppFacade() as unknown as Application 17 | const View: ViewEngine = new ViewFacade() as unknown as ViewEngine 18 | const Crypt: Encrypter = new CryptFacade() as unknown as Encrypter 19 | const Route: HttpRouter = new RouteFacade() as unknown as HttpRouter 20 | const Config: ConfigStore = new ConfigFacade() as unknown as ConfigStore 21 | const Database: DatabaseContract = new DatabaseFacade() as unknown as DatabaseContract 22 | 23 | export { Facade } from './facade.js' 24 | export { 25 | App, 26 | Config, 27 | Crypt, 28 | Database, 29 | Env, 30 | Hash, 31 | Log as Logger, 32 | Route, 33 | View 34 | } 35 | -------------------------------------------------------------------------------- /packages/facades/src/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class LogFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'logger' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/route.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class RouteFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'route' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/src/view.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Facade } from './facade.js' 3 | 4 | export class ViewFacade extends Facade { 5 | /** 6 | * Returns the container binding name. 7 | */ 8 | override getContainerNamespace (): string { 9 | return 'view' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/facades/test/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | 5 | test.skip('TODO', () => { 6 | expect(true).toBeFalse() 7 | }) 8 | 9 | test.run() 10 | -------------------------------------------------------------------------------- /packages/facades/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/hashing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/hashing", 3 | "description": "The Supercharge hashing package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js", 17 | "./drivers/bcrypt": "./dist/bcrypt-hasher.js", 18 | "./drivers/scrypt": "./dist/scrypt-hasher.js" 19 | }, 20 | "dependencies": { 21 | "@phc/format": "~1.0.0", 22 | "@supercharge/contracts": "^4.0.0-alpha.2", 23 | "@supercharge/manager": "^4.0.0-alpha.2", 24 | "@supercharge/support": "^4.0.0-alpha.2" 25 | }, 26 | "devDependencies": { 27 | "@supercharge/application": "^4.0.0-alpha.2", 28 | "@types/bcrypt": "^5.0.2", 29 | "bcrypt": "~5.1.1", 30 | "c8": "~9.1.0", 31 | "expect": "~29.7.0", 32 | "typescript": "~5.4.5", 33 | "uvu": "~0.5.6" 34 | }, 35 | "engines": { 36 | "node": ">=22" 37 | }, 38 | "homepage": "https://superchargejs.com", 39 | "keywords": [ 40 | "hashing", 41 | "bcrypt", 42 | "scrypt", 43 | "supercharge", 44 | "superchargejs", 45 | "nodejs" 46 | ], 47 | "license": "MIT", 48 | "publishConfig": { 49 | "access": "public" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "directory": "packages/hashing", 54 | "url": "git+https://github.com/supercharge/framework.git" 55 | }, 56 | "scripts": { 57 | "build": "tsc --build tsconfig.json --force", 58 | "watch": "npm run build -- --watch", 59 | "lint": "eslint src --ext .js,.ts", 60 | "lint:fix": "npm run lint -- --fix", 61 | "test": "npm run build && npm run lint && npm run test:run", 62 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 63 | "posttest": "c8 report --reporter=html" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/hashing/src/bcrypt-hasher.ts: -------------------------------------------------------------------------------- 1 | 2 | import Bcrypt from 'bcrypt' 3 | import { BaseHasher } from './base-hasher.js' 4 | import { Hasher as HasherContract, HashConfig } from '@supercharge/contracts' 5 | 6 | export class BcryptHasher extends BaseHasher implements HasherContract { 7 | /** 8 | * The cost factor. 9 | */ 10 | private readonly rounds: number = 12 11 | 12 | /** 13 | * Create a new instance. 14 | */ 15 | constructor ({ rounds }: HashConfig['bcrypt'] = {}) { 16 | super() 17 | 18 | this.rounds = rounds ?? this.rounds 19 | } 20 | 21 | /** 22 | * Hash the given `value`. 23 | */ 24 | async make (value: string): Promise { 25 | return await Bcrypt.hash(value, this.rounds) 26 | } 27 | 28 | /** 29 | * Compare a the `plain` text value against the given `hashedValue`. 30 | */ 31 | async check (plain: string, hashedValue: string): Promise { 32 | return await Bcrypt.compare(plain, hashedValue) 33 | } 34 | 35 | /** 36 | * Determine whether the given hash value has been hashed using the configured options. 37 | */ 38 | needsRehash (hashedValue: string): boolean { 39 | if (typeof hashedValue !== 'string') { 40 | throw new Error('You must provide a string value as an argument to the "needsRehash" method.') 41 | } 42 | 43 | const [version, rounds] = hashedValue.split('$').slice(1) 44 | 45 | return this.rounds !== Number(rounds) || version !== '2b' 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/hashing/src/hash-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BinaryToTextEncoding, Encoding } from 'node:crypto' 3 | import { HashBuilder as HashBuilderContract, HashBuilderConfig } from '@supercharge/contracts' 4 | 5 | export class HashBuilder implements HashBuilderContract { 6 | /** 7 | * Stores the hash builder config. 8 | */ 9 | private readonly config: HashBuilderConfig 10 | 11 | /** 12 | * Create a new instance. 13 | */ 14 | constructor (options: HashBuilderConfig) { 15 | this.config = options 16 | } 17 | 18 | /** 19 | * Set the input encoding for the related value. 20 | */ 21 | inputEncoding (inputEncoding: Encoding): this { 22 | this.config.inputEncoding = inputEncoding 23 | return this 24 | } 25 | 26 | /** 27 | * Calculate the final hash string value. 28 | */ 29 | digest (encoding: BinaryToTextEncoding): void { 30 | this.config.outputEncoding = encoding 31 | } 32 | 33 | /** 34 | * This is an alias for the `digest` method that calculates the final hash string. 35 | */ 36 | toString (encoding: BinaryToTextEncoding): void { 37 | this.digest(encoding) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/hashing/src/hashing-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HashManager } from './hash-manager.js' 3 | import { ServiceProvider } from '@supercharge/support' 4 | 5 | /** 6 | * Add container bindings for services from this provider. 7 | */ 8 | declare module '@supercharge/contracts' { 9 | export interface ContainerBindings { 10 | 'hash': HashManager 11 | } 12 | } 13 | 14 | export class HashingServiceProvider extends ServiceProvider { 15 | /** 16 | * Register the hash manager into the container. 17 | */ 18 | override register (): void { 19 | this.registerHashManager() 20 | } 21 | 22 | /** 23 | * Register the encrypter instance. 24 | */ 25 | protected registerHashManager (): void { 26 | this.app().singleton('hash', () => { 27 | return new HashManager(this.app()) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/hashing/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { HashManager } from './hash-manager.js' 3 | export { ScryptValidationError } from './scrypt-validation-error.js' 4 | export { HashingServiceProvider } from './hashing-service-provider.js' 5 | -------------------------------------------------------------------------------- /packages/hashing/src/missing-hasher-error.ts: -------------------------------------------------------------------------------- 1 | import { HashConfig } from '@supercharge/contracts' 2 | 3 | export class MissingHasherError extends TypeError { 4 | /** 5 | * Create a new instance. 6 | */ 7 | constructor (driver: HashConfig['driver'], message?: string) { 8 | super(message ?? `Missing hasher constructor for driver "${driver}"`) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/hashing/src/scrypt-validation-error.ts: -------------------------------------------------------------------------------- 1 | 2 | export class ScryptValidationError extends TypeError { 3 | // 4 | } 5 | -------------------------------------------------------------------------------- /packages/hashing/test/bcrypt-hasher.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { BcryptHasher } from '../dist/bcrypt-hasher.js' 5 | 6 | test('defaults to 12 rounds', async () => { 7 | const bcrypt = new BcryptHasher({}) 8 | const value = await bcrypt.make('Supercharge') 9 | 10 | expect(typeof value === 'string').toBe(true) 11 | expect(value.startsWith('$2b')).toBe(true) 12 | }) 13 | 14 | test('needsRehash when changing the configuration', async () => { 15 | const bcrypt10 = new BcryptHasher({ rounds: 10 }) 16 | const bcrypt12 = new BcryptHasher({ rounds: 12 }) 17 | 18 | const value10 = await bcrypt10.make('Supercharge') 19 | 20 | expect(bcrypt10.needsRehash(value10)).toBe(false) 21 | expect(bcrypt12.needsRehash(value10)).toBe(true) 22 | }) 23 | 24 | test.run() 25 | -------------------------------------------------------------------------------- /packages/hashing/test/hashing-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Path from 'node:path' 4 | import { expect } from 'expect' 5 | import { fileURLToPath } from 'node:url' 6 | import { Application } from '@supercharge/application' 7 | import { HashManager, HashingServiceProvider } from '../dist/index.js' 8 | 9 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 10 | const fixturesPath = Path.resolve(__dirname, './fixtures') 11 | 12 | const app = Application.createWithAppRoot(fixturesPath) 13 | 14 | test('register hash manager', async () => { 15 | app.register(new HashingServiceProvider(app)) 16 | 17 | expect(app.make('hash')).toBeInstanceOf(HashManager) 18 | }) 19 | 20 | test.run() 21 | -------------------------------------------------------------------------------- /packages/hashing/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { Application } from '@supercharge/application' 5 | import { BcryptHasher } from '../../dist/bcrypt-hasher.js' 6 | import { ScryptHasher } from '../../dist/scrypt-hasher.js' 7 | import { HashingServiceProvider } from '../../dist/index.js' 8 | 9 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 10 | const fixturesPath = Path.resolve(__dirname, './fixtures') 11 | 12 | /** 13 | * Returns a test application. 14 | * 15 | * @param {import('@supercharge/contracts').HashConfig} [hashConfig] 16 | * 17 | * @returns {Promise} 18 | */ 19 | export async function setupApp (hashConfig = {}) { 20 | const app = Application.createWithAppRoot(fixturesPath) 21 | 22 | app.config() 23 | .set('app.key', 'app-key-1234') 24 | .set('hashing', { 25 | driver: 'bcrypt', 26 | drivers: { 27 | bcrypt: BcryptHasher, 28 | scrypt: ScryptHasher 29 | }, 30 | bcrypt: { 31 | rounds: 10, 32 | ...hashConfig.bcrypt 33 | }, 34 | ...hashConfig 35 | }) 36 | 37 | await app 38 | .register(new HashingServiceProvider(app)) 39 | .boot() 40 | 41 | return app 42 | } 43 | -------------------------------------------------------------------------------- /packages/hashing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../application" 17 | }, 18 | { 19 | "path": "../contracts" 20 | }, 21 | { 22 | "path": "../manager" 23 | }, 24 | { 25 | "path": "../support" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/http/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | Middleware, 4 | BodyparserMiddleware, 5 | BodyparserBaseOptions, 6 | BodyparserJsonOptions, 7 | BodyparserMultipartOptions, 8 | BodyparserOptions, 9 | HandleCorsMiddleware, 10 | ServeStaticAssetsMiddleware 11 | } from './middleware/index.js' 12 | 13 | export { 14 | RouteGroup, 15 | PendingRoute, 16 | RouteCollection, 17 | Route, 18 | Router 19 | } from './routing/index.js' 20 | 21 | export { 22 | CookieBag, 23 | Controller, 24 | FileBag, 25 | HttpContext, 26 | HttpRedirect, 27 | InputBag, 28 | Request, 29 | RequestHeaderBag, 30 | Response, 31 | ResponseHeaderBag, 32 | Server, 33 | StateBag, 34 | UploadedFile 35 | } from './server/index.js' 36 | 37 | export { HttpServiceProvider } from './http-service-provider.js' 38 | -------------------------------------------------------------------------------- /packages/http/src/middleware/base.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application, HttpContext, Middleware as MiddlewareContract, NextHandler } from '@supercharge/contracts' 3 | 4 | export abstract class Middleware implements MiddlewareContract { 5 | /** 6 | * Stores the app instance. 7 | */ 8 | protected readonly app: Application 9 | 10 | /** 11 | * Create a new middleware instance. 12 | */ 13 | constructor (app: Application) { 14 | this.app = app 15 | } 16 | 17 | /** 18 | * Run the middleware processing on the given HTTP `ctx`. 19 | */ 20 | abstract handle (ctx: HttpContext, next: NextHandler): Promise | any 21 | } 22 | -------------------------------------------------------------------------------- /packages/http/src/middleware/bodyparser/bodyparser-base-options.ts: -------------------------------------------------------------------------------- 1 | 2 | import Bytes from 'bytes' 3 | import Set from '@supercharge/set' 4 | 5 | interface BodyparserBaseOptionsContract { 6 | limit?: number | string 7 | contentTypes: string[] 8 | maxFileSize?: number | string 9 | } 10 | 11 | export class BodyparserBaseOptions { 12 | /** 13 | * The bodyparser base options object. 14 | */ 15 | protected readonly config: BodyparserBaseOptionsContract 16 | 17 | /** 18 | * Create a new instance. 19 | */ 20 | constructor (options: BodyparserBaseOptionsContract = { contentTypes: [] }) { 21 | this.config = options 22 | } 23 | 24 | /** 25 | * Returns the JSON body size limit in bytes. 26 | */ 27 | limit (): number { 28 | return Bytes.parse(this.config.limit ?? '56kb') 29 | } 30 | 31 | /** 32 | * Returns the allowed JSON content types 33 | */ 34 | contentTypes (): string[] { 35 | return Set 36 | .from(this.config.contentTypes) 37 | .map(contentType => contentType.toLowerCase()) 38 | .toArray() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/http/src/middleware/bodyparser/bodyparser-json-options.ts: -------------------------------------------------------------------------------- 1 | 2 | import Bytes from 'bytes' 3 | import { BodyparserBaseOptions } from './bodyparser-base-options.js' 4 | 5 | export class BodyparserJsonOptions extends BodyparserBaseOptions { 6 | /** 7 | * Returns the JSON body size limit in bytes. 8 | */ 9 | override limit (): number { 10 | return Bytes.parse(this.config.limit ?? '1mb') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/http/src/middleware/bodyparser/bodyparser-multipart-options.ts: -------------------------------------------------------------------------------- 1 | 2 | import Bytes from 'bytes' 3 | import { BodyparserBaseOptions } from './bodyparser-base-options.js' 4 | import { BodyparserConfig as BodyparserConfigContract } from '@supercharge/contracts' 5 | 6 | export class BodyparserMultipartOptions extends BodyparserBaseOptions { 7 | /** 8 | * The bodyparser multipart options object. 9 | */ 10 | protected override readonly config: BodyparserConfigContract['multipart'] 11 | 12 | /** 13 | * Create a new instance. 14 | */ 15 | constructor (config: BodyparserConfigContract['multipart']) { 16 | super(config) 17 | 18 | this.config = config 19 | } 20 | 21 | /** 22 | * Returns the multipart file size limit in bytes for a single file. 23 | */ 24 | maxFileSize (): number { 25 | return Bytes.parse(this.config.maxFileSize ?? '200mb') 26 | } 27 | 28 | /** 29 | * Returns the multipart file size limit in bytes for all uploaded files. 30 | */ 31 | maxTotalFileSize (): number { 32 | return Bytes.parse(this.config.maxTotalFileSize ?? this.maxFileSize()) 33 | } 34 | 35 | /** 36 | * Returns the maximun number of allowed fields in the multipart body. 37 | */ 38 | maxFields (): number { 39 | return this.config.maxFields ?? 1000 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/http/src/middleware/bodyparser/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './bodyparser.js' 3 | export * from './bodyparser-options.js' 4 | export * from './bodyparser-base-options.js' 5 | export * from './bodyparser-json-options.js' 6 | export * from './bodyparser-multipart-options.js' 7 | -------------------------------------------------------------------------------- /packages/http/src/middleware/handle-cors.ts: -------------------------------------------------------------------------------- 1 | 2 | import Koa from 'koa' 3 | import cors from '@koa/cors' 4 | import { Middleware } from './base.js' 5 | import { Application, CorsConfig, HttpContext, NextHandler } from '@supercharge/contracts' 6 | 7 | export class HandleCorsMiddleware extends Middleware { 8 | /** 9 | * The CORS handler for incoming requests. 10 | */ 11 | private readonly handleCors: Koa.Middleware 12 | 13 | /** 14 | * Create a new middleware instance. 15 | */ 16 | constructor (app: Application) { 17 | super(app) 18 | 19 | this.handleCors = cors(this.createConfig()) 20 | } 21 | 22 | /** 23 | * Returns the options determining how to serve assets. 24 | */ 25 | protected createConfig (): cors.Options { 26 | const config = this.config() 27 | 28 | return { 29 | maxAge: config.maxAge, 30 | keepHeadersOnError: true, 31 | origin: config.allowedOrigin, 32 | allowMethods: config.allowedMethods, 33 | allowHeaders: config.allowedHeaders, 34 | exposeHeaders: config.exposedHeaders, 35 | credentials: config.supportsCredentials 36 | } 37 | } 38 | 39 | /** 40 | * Returns the CORS config object. 41 | */ 42 | config (): CorsConfig { 43 | return this.app.config().get('cors') 44 | } 45 | 46 | /** 47 | * Handle the incoming request. 48 | */ 49 | async handle (ctx: HttpContext, next: NextHandler): Promise { 50 | return await this.handleCors(ctx.raw, next) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/http/src/middleware/handle-error.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Middleware } from './base.js' 3 | import { HttpContext, NextHandler, ErrorHandler } from '@supercharge/contracts' 4 | 5 | export class HandleErrorMiddleware extends Middleware { 6 | /** 7 | * Handle the incoming request. 8 | */ 9 | async handle (ctx: HttpContext, next: NextHandler): Promise { 10 | try { 11 | await next() 12 | } catch (error: any) { 13 | await this.handleError(error, ctx) 14 | } 15 | } 16 | 17 | /** 18 | * Process the given `error` and HTTP `ctx` using the error handler. 19 | */ 20 | private async handleError (error: Error, ctx: HttpContext): Promise { 21 | if (this.app.hasBinding('error.handler')) { 22 | return await this.app.make('error.handler').handle(error, ctx) 23 | } 24 | 25 | throw error 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/http/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './base.js' 3 | export * from './bodyparser/index.js' 4 | export * from './handle-cors.js' 5 | export * from './serve-static-assets.js' 6 | -------------------------------------------------------------------------------- /packages/http/src/middleware/serve-static-assets.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Middleware } from './base.js' 3 | import serveStaticFilesFrom from 'koa-static' 4 | import { Application, HttpContext, NextHandler, StaticAssetsConfig } from '@supercharge/contracts' 5 | 6 | export class ServeStaticAssetsMiddleware extends Middleware { 7 | /** 8 | * The asset handler serving static files for an incoming request. 9 | */ 10 | private readonly handleAssets: any 11 | 12 | /** 13 | * Create a new middleware instance. 14 | */ 15 | constructor (app: Application) { 16 | super(app) 17 | 18 | this.handleAssets = serveStaticFilesFrom( 19 | this.assetsLocation(), this.config() 20 | ) 21 | } 22 | 23 | /** 24 | * Returns the path to the asset files. 25 | */ 26 | assetsLocation (): string { 27 | return this.app.publicPath() 28 | } 29 | 30 | /** 31 | * Returns the options determining how to serve assets. 32 | */ 33 | config (): StaticAssetsConfig { 34 | return this.app.config().get('static') 35 | } 36 | 37 | /** 38 | * Handle the incoming request. 39 | */ 40 | async handle (ctx: HttpContext, next: NextHandler): Promise { 41 | return await this.handleAssets(ctx.raw, next) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/http/src/routing/group.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Str } from '@supercharge/strings' 3 | import { HttpRouteGroup, RouteAttributes } from '@supercharge/contracts' 4 | 5 | export class RouteGroup implements HttpRouteGroup { 6 | /** 7 | * Stores the route group attributes, like prefix and middleware. 8 | */ 9 | private readonly attributes: RouteAttributes 10 | 11 | /** 12 | * Create a new route group instance. 13 | */ 14 | constructor (attributes: RouteAttributes) { 15 | this.attributes = attributes 16 | } 17 | 18 | /** 19 | * Returns the route group prefix. 20 | */ 21 | prefix (): string { 22 | return Str(this.attributes.prefix).ltrim('/').start('/').get() 23 | } 24 | 25 | /** 26 | * Returns the route group middleware stack. 27 | */ 28 | middleware (): string[] { 29 | return this.attributes.middleware ?? [] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/http/src/routing/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './group.js' 3 | export * from './pending-route.js' 4 | export * from './route-collection.js' 5 | export * from './route.js' 6 | export * from './router.js' 7 | -------------------------------------------------------------------------------- /packages/http/src/routing/route-collection.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Route } from './route.js' 3 | import { tap } from '@supercharge/goodies' 4 | import { HttpRouteCollection, RouteObjectAttributes } from '@supercharge/contracts' 5 | 6 | export class RouteCollection implements HttpRouteCollection { 7 | /** 8 | * Stores all registered routes. 9 | */ 10 | private routes: Route[] 11 | 12 | /** 13 | * Create a new instance. 14 | */ 15 | constructor () { 16 | this.routes = [] 17 | } 18 | 19 | /** 20 | * Register a new route to the collection. 21 | */ 22 | public add (route: Route | Route[]): Route | Route[] { 23 | return tap(route, () => { 24 | this.routes = this.routes.concat(route) 25 | }) 26 | } 27 | 28 | /** 29 | * Returns all registered routes. 30 | */ 31 | public all (): Route[] { 32 | return this.routes 33 | } 34 | 35 | /** 36 | * Clear all registered routes. 37 | */ 38 | public clear (): this { 39 | return tap(this, () => { 40 | this.routes = [] 41 | }) 42 | } 43 | 44 | /** 45 | * Returns the number of routes registerd to this route collection. 46 | */ 47 | public count (): number { 48 | return this.routes.length 49 | } 50 | 51 | /** 52 | * Returns the attributes of all routes. 53 | */ 54 | public toJSON (): RouteObjectAttributes[] { 55 | return this.routes.map(route => { 56 | return route.toJSON() 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/http/src/server/controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Application, HttpContext, Logger } from '@supercharge/contracts' 3 | 4 | export abstract class Controller { 5 | /** 6 | * The application instance. 7 | */ 8 | protected readonly app: Application 9 | 10 | /** 11 | * Create a new controller instance. 12 | */ 13 | constructor (app: Application) { 14 | this.app = app 15 | } 16 | 17 | /** 18 | * Returns the application logger instance. 19 | */ 20 | protected logger (): Logger { 21 | return this.app.make('logger') 22 | } 23 | 24 | /** 25 | * Handle the incoming HTTP request. 26 | */ 27 | abstract handle (ctx: HttpContext): Promise | any 28 | } 29 | -------------------------------------------------------------------------------- /packages/http/src/server/cookies/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './request-cookie-builder.js' 3 | export * from './response-cookie-builder.js' 4 | -------------------------------------------------------------------------------- /packages/http/src/server/cookies/request-cookie-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | import { tap } from '@supercharge/goodies' 3 | import { CookieConfig, RequestCookieBuilder as RequestCookieContract } from '@supercharge/contracts' 4 | 5 | export class RequestCookieBuilder implements RequestCookieContract { 6 | /** 7 | * Stores the options used when retrieving a cookie value from the request. 8 | */ 9 | private readonly cookieConfig: CookieConfig 10 | 11 | /** 12 | * Create a new instance. 13 | */ 14 | constructor (options: CookieConfig) { 15 | this.cookieConfig = options 16 | } 17 | 18 | /** 19 | * Retrieve the unsigned cookie value. 20 | */ 21 | unsigned (): this { 22 | return tap(this, () => { 23 | this.cookieConfig.signed = false 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/http/src/server/http-redirect.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | import { tap } from '@supercharge/goodies' 4 | import { HttpRedirect as HttpRedirectContract } from '@supercharge/contracts' 5 | 6 | export class HttpRedirect implements HttpRedirectContract { 7 | /** 8 | * The route context object from Koa. 9 | */ 10 | private readonly ctx: Context 11 | 12 | /** 13 | * Create a new instance. 14 | */ 15 | constructor (ctx: Context) { 16 | this.ctx = ctx 17 | } 18 | 19 | /** 20 | * Redirect the request back to the previous path. 21 | */ 22 | back (options?: { fallback: string }): this { 23 | const fallback = options ? options.fallback : '' 24 | 25 | this.ctx.response.redirect('back', fallback) 26 | 27 | return this 28 | } 29 | 30 | /** 31 | * Redirect the request to the given URL `path`. 32 | */ 33 | to (path: string): this { 34 | return tap(this, () => { 35 | this.ctx.response.redirect(path) 36 | }) 37 | } 38 | 39 | /** 40 | * Redirects the request with HTTP status 307. This keeps the request payload 41 | * which is useful for POST/PUT requests containing content. 42 | * 43 | * More details: Details: https://developer.mozilla.org/de/docs/Web/HTTP/Status 44 | */ 45 | withPayload (): this { 46 | return tap(this, () => { 47 | this.ctx.response.status = 307 48 | }) 49 | } 50 | 51 | /** 52 | * Marks this redirect as permanent with HTTP status 301. 53 | */ 54 | permanent (): this { 55 | return tap(this, () => { 56 | this.ctx.response.status = 301 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/http/src/server/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './cookie-bag.js' 3 | export * from './controller.js' 4 | export * from './file-bag.js' 5 | export * from './http-context.js' 6 | export * from './http-redirect.js' 7 | export * from './input-bag.js' 8 | export * from './request.js' 9 | export * from './request-header-bag.js' 10 | export * from './response.js' 11 | export * from './response-header-bag.js' 12 | export * from './server.js' 13 | export * from './state-bag.js' 14 | export * from './uploaded-file.js' 15 | -------------------------------------------------------------------------------- /packages/http/src/server/interacts-with-state.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StateBag } from './state-bag.js' 3 | import { InputBag } from './input-bag.js' 4 | import { tap } from '@supercharge/goodies' 5 | import { RouterContext } from '@koa/router' 6 | import { InteractsWithState as InteractsWithStateContract, HttpStateData } from '@supercharge/contracts' 7 | 8 | export class InteractsWithState implements InteractsWithStateContract { 9 | /** 10 | * The route context object from Koa. 11 | */ 12 | protected readonly koaCtx: RouterContext 13 | 14 | /** 15 | * Create a new instance. 16 | */ 17 | constructor (ctx: RouterContext) { 18 | this.koaCtx = ctx 19 | } 20 | 21 | /** 22 | * Returns the shared state bag for this HTTP context. 23 | */ 24 | state (): StateBag { 25 | return new InputBag(this.koaCtx.state) 26 | } 27 | 28 | /** 29 | * Share a given `state` across HTTP requests. Any previously 30 | * set state will be merged with the given `state`. 31 | */ 32 | share (key: Key, value: HttpStateData[Key]): this 33 | share (values: Partial): this 34 | share (key: Key | HttpStateData, value?: any): this { 35 | return tap(this, () => { 36 | this.state().set(key, value) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/http/src/server/query-parameter-bag.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { InputBag } from './input-bag.js' 4 | import { QueryParameterBag as QueryParameterBagContract } from '@supercharge/contracts' 5 | 6 | export class QueryParameterBag extends InputBag implements QueryParameterBagContract { 7 | /** 8 | * Returns the query string created from all items in this query parameter bag, 9 | * without the leading question mark `?`. 10 | * 11 | * **Notice:** the returned querystring is encoded. Node.js automatically 12 | * encodes the querystring to ensure a valid URL. Some characters would 13 | * break the URL string otherwise. This way ensures the valid string. 14 | */ 15 | toQuerystring (): string { 16 | return new URLSearchParams( 17 | this.all() as Record 18 | ).toString() 19 | } 20 | 21 | /** 22 | * Returns the decoded querystring by running the result of `toQuerystring` 23 | * through `decodeURIComponent`. This method is useful to debug during 24 | * development. It’s recommended to use `toQuerystring` in production. 25 | */ 26 | toQuerystringDecoded (): string { 27 | return decodeURIComponent( 28 | this.toQuerystring() 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/http/src/server/state-bag.ts: -------------------------------------------------------------------------------- 1 | 2 | import _ from 'lodash' 3 | import { InputBag } from './input-bag.js' 4 | import { StateBag as StateBagContract, HttpStateData } from '@supercharge/contracts' 5 | 6 | export class StateBag extends InputBag implements StateBagContract { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /packages/http/src/server/uploaded-file.ts: -------------------------------------------------------------------------------- 1 | 2 | import { File as FormidableFile } from 'formidable' 3 | import { UploadedFile as UploadedFileContract } from '@supercharge/contracts' 4 | 5 | export class UploadedFile implements UploadedFileContract { 6 | /** 7 | * Stores the raw formidable file from the request. 8 | */ 9 | private readonly file: FormidableFile 10 | 11 | /** 12 | * Create a new instance. 13 | */ 14 | constructor (file: FormidableFile) { 15 | this.file = file 16 | } 17 | 18 | /** 19 | * Returns the file name (according to the uploading client). 20 | */ 21 | name (): string | undefined { 22 | if (this.file.originalFilename) { 23 | return this.file.originalFilename 24 | } 25 | 26 | return undefined 27 | } 28 | 29 | /** 30 | * Returns the current file path of this uploaded file. Uploaded files 31 | * are stored in a temporary location of the operating system. 32 | */ 33 | path (): string { 34 | return this.file.filepath 35 | } 36 | 37 | /** 38 | * Returns the file size in bytes. 39 | */ 40 | size (): number { 41 | return this.file.size 42 | } 43 | 44 | /** 45 | * Returns the file’s mime type (according to the uploading client). 46 | */ 47 | mimeType (): string | undefined { 48 | if (this.file.mimetype) { 49 | return this.file.mimetype 50 | } 51 | 52 | return undefined 53 | } 54 | 55 | /** 56 | * Return a `Date` instance containing the time this file was last written to. 57 | */ 58 | lastModified (): Date | null | undefined { 59 | return this.file.mtime 60 | } 61 | 62 | /** 63 | * Returns the JSON object of this file. 64 | */ 65 | toJSON (): { [key: string]: any } { 66 | return { 67 | name: this.name(), 68 | size: this.size(), 69 | path: this.path(), 70 | mimeType: this.mimeType(), 71 | lastModified: this.lastModified() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/http/test/controller.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { setupApp } from './helpers/index.js' 5 | import { Controller } from '../dist/index.js' 6 | 7 | const app = setupApp() 8 | 9 | test('controller.app()', async () => { 10 | class TestController extends Controller {} 11 | const controller = new TestController(app) 12 | 13 | expect(controller.app).toEqual(app) 14 | }) 15 | 16 | test.run() 17 | -------------------------------------------------------------------------------- /packages/http/test/file-bag.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { FileBag } from '../dist/index.js' 5 | 6 | test('all', () => { 7 | expect(new FileBag().all()).toEqual({}) 8 | expect(new FileBag({}).all()).toEqual({}) 9 | expect( 10 | new FileBag({ accept: 'application/json' }).all() 11 | ).toEqual({ accept: 'application/json' }) 12 | }) 13 | 14 | test('all for keys', () => { 15 | const bag = new FileBag({ a: 1, b: 2, c: 3 }) 16 | 17 | expect(bag.all('a')).toEqual({ a: 1 }) 18 | expect(bag.all('a', 'b')).toEqual({ a: 1, b: 2 }) 19 | expect(bag.all(['a', 'b'])).toEqual({ a: 1, b: 2 }) 20 | }) 21 | 22 | test('get', () => { 23 | expect(new FileBag({}).get()).toEqual(undefined) 24 | expect(new FileBag().get('test')).toEqual(undefined) 25 | expect(new FileBag({ a: 'b' }).get('c')).toEqual(undefined) 26 | expect(new FileBag({ a: 'b' }).get('a')).toEqual('b') 27 | }) 28 | 29 | test('add', () => { 30 | expect(new FileBag().add('a', 1).all()).toEqual({ a: 1 }) 31 | expect(new FileBag({}).add('a', 1).all()).toEqual({ a: 1 }) 32 | 33 | expect(new FileBag({ a: 'b' }).add('c', 1).all()).toEqual({ a: 'b', c: 1 }) 34 | 35 | const bag = new FileBag({ a: 'b' }) 36 | expect(bag.get('a')).toEqual('b') 37 | expect(bag.add('a', 1).get('a')).toEqual(1) 38 | }) 39 | 40 | test('has', () => { 41 | expect(new FileBag({}).has()).toBe(false) 42 | expect(new FileBag().has('test')).toBe(false) 43 | expect(new FileBag({ a: 'b' }).has('c')).toBe(false) 44 | 45 | expect(new FileBag({ a: 'b' }).has('a')).toBe(true) 46 | }) 47 | 48 | test('has', () => { 49 | expect(new FileBag().isEmpty()).toBe(true) 50 | expect(new FileBag({}).isEmpty()).toBe(true) 51 | 52 | expect(new FileBag({ a: 'b' }).isEmpty()).toBe(false) 53 | 54 | const bag = new FileBag() 55 | expect(bag.isEmpty()).toBe(true) 56 | expect(bag.add('a', 1).isEmpty()).toBe(false) 57 | }) 58 | 59 | test.run() 60 | -------------------------------------------------------------------------------- /packages/http/test/helpers/error-handler.js: -------------------------------------------------------------------------------- 1 | 2 | export default class ErrorHandler { 3 | handle (error, ctx) { 4 | // console.log('Received error in testing error handler', { error }) 5 | 6 | ctx.response.status(error.status || error.statusCode || 500) 7 | ctx.response.payload(error.message) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/http/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Path from 'node:path' 3 | import deepmerge from 'deepmerge' 4 | import { fileURLToPath } from 'node:url' 5 | import ErrorHandler from './error-handler.js' 6 | import { Application } from '@supercharge/application' 7 | import { ViewServiceProvider } from '@supercharge/view' 8 | import { HttpServiceProvider } from '../../dist/index.js' 9 | 10 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 11 | const appRoot = Path.resolve(__dirname, './') 12 | 13 | const defaultOptions = { 14 | appRoot 15 | } 16 | 17 | /** 18 | * Returns a test application. 19 | * 20 | * @param {import('@supercharge/contracts').HttpConfig} [httpConfig] 21 | * 22 | * @returns {Application} 23 | */ 24 | export function setupApp (config) { 25 | config = deepmerge(defaultOptions, { ...config }) 26 | 27 | const app = Application 28 | .createWithAppRoot(config.appRoot) 29 | .withErrorHandler(ErrorHandler) 30 | 31 | app.config() 32 | .set('app', deepmerge({ 33 | key: 'app-key-1234' 34 | }, { ...config.app })) 35 | .set('http', deepmerge({ 36 | host: 'localhost', 37 | port: 7337, 38 | cookie: {} 39 | }, { ...config.http })) 40 | .set('view', deepmerge({ 41 | driver: 'handlebars', 42 | handlebars: { 43 | views: app.resourcePath('views'), 44 | partials: app.resourcePath('views/partials'), 45 | helpers: app.resourcePath('views/helpers') 46 | // layouts: app.resourcePath('views/layouts') 47 | // defaultLayout: 'app' 48 | } 49 | }, { ...config.view })) 50 | .set('cors', deepmerge({}, { ...config.cors })) 51 | .set('static', deepmerge({}, { ...config.static })) 52 | .set('bodyparser', deepmerge({}, { ...config.bodyparser })) 53 | 54 | return app 55 | .register(new HttpServiceProvider(app)) 56 | .register(new ViewServiceProvider(app)) 57 | } 58 | -------------------------------------------------------------------------------- /packages/http/test/http-context.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Supertest from 'supertest' 4 | import { Server } from '../dist/index.js' 5 | import { setupApp } from './helpers/index.js' 6 | 7 | let app = setupApp() 8 | 9 | test.before.each(() => { 10 | app = setupApp() 11 | }) 12 | 13 | test('returns the raw context', async () => { 14 | const server = app.make(Server).use(({ response, raw }) => { 15 | return response.payload({ 16 | hasRaw: !!raw 17 | }) 18 | }) 19 | 20 | await Supertest(server.callback()) 21 | .get('/') 22 | .expect(200, { hasRaw: true }) 23 | }) 24 | 25 | test.run() 26 | -------------------------------------------------------------------------------- /packages/http/test/http-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { setupApp } from './helpers/index.js' 5 | import { HttpServiceProvider, Server } from '../dist/index.js' 6 | 7 | const app = setupApp() 8 | 9 | test('register', async () => { 10 | const provider = new HttpServiceProvider(app) 11 | provider.register() 12 | 13 | expect(app.hasBinding('server')).toBe(true) 14 | expect(app.hasBinding(Server)).toBe(true) 15 | expect(app.make('server')).toBeInstanceOf(Server) 16 | expect(app.make(Server)).toBeInstanceOf(Server) 17 | }) 18 | 19 | test('shutdown', async () => { 20 | const provider = new HttpServiceProvider(app) 21 | const server = app.make(Server) 22 | 23 | await server 24 | .booted(async () => { 25 | await provider.shutdown() 26 | }) 27 | .start() 28 | }) 29 | 30 | test.run() 31 | -------------------------------------------------------------------------------- /packages/http/test/input-bag.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { InputBag } from '../dist/index.js' 5 | 6 | test('all', () => { 7 | expect(new InputBag({}).all()).toEqual({}) 8 | expect( 9 | new InputBag({ accept: 'application/json' }).all() 10 | ).toEqual({ accept: 'application/json' }) 11 | }) 12 | 13 | test('all for keys', () => { 14 | const bag = new InputBag({ a: 1, b: 2, c: 3 }) 15 | 16 | expect(bag.all('a')).toEqual({ a: 1 }) 17 | expect(bag.all('a', 'b')).toEqual({ a: 1, b: 2 }) 18 | expect(bag.all(['a', 'b'])).toEqual({ a: 1, b: 2 }) 19 | }) 20 | 21 | test('get', () => { 22 | expect(new InputBag({}).get()).toEqual(undefined) 23 | expect(new InputBag({}).get('test')).toEqual(undefined) 24 | expect(new InputBag({ a: 'b' }).get('c')).toEqual(undefined) 25 | expect(new InputBag({ a: 'b' }).get('a')).toEqual('b') 26 | }) 27 | 28 | test('set', () => { 29 | expect(new InputBag({}).set('a', 1).all()).toEqual({ a: 1 }) 30 | 31 | expect(new InputBag({ a: 'b' }).set('c', 1).all()).toEqual({ a: 'b', c: 1 }) 32 | 33 | const bag = new InputBag({ a: 'b' }) 34 | expect(bag.get('a')).toEqual('b') 35 | expect(bag.set('a', 1).get('a')).toEqual(1) 36 | }) 37 | 38 | test('has', () => { 39 | expect(new InputBag({}).has()).toBe(false) 40 | expect(new InputBag({}).has('test')).toBe(false) 41 | expect(new InputBag({ a: 'b' }).has('c')).toBe(false) 42 | 43 | expect(new InputBag({ a: 'b' }).has('a')).toBe(true) 44 | }) 45 | 46 | test('remove', () => { 47 | expect(new InputBag({}).remove('a').all()).toEqual({ }) 48 | expect(new InputBag({}).remove('a').all()).toEqual({ }) 49 | expect(new InputBag({ a: 1, b: 2 }).remove('a').all()).toEqual({ b: 2 }) 50 | expect(new InputBag({ a: 1, b: 2 }).remove('c').all()).toEqual({ a: 1, b: 2 }) 51 | }) 52 | 53 | test.run() 54 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/bodyparser-json-options.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { BodyparserOptions } from '../../../dist/index.js' 5 | 6 | test('limit defaults to 1mb', () => { 7 | expect(new BodyparserOptions({ }).json().limit()).toEqual( 8 | 1024 * 1024 // 1mb in bytes 9 | ) 10 | }) 11 | 12 | test('limit', () => { 13 | expect(new BodyparserOptions({ json: { limit: 10 } }).json().limit()).toEqual(10) 14 | expect(new BodyparserOptions({ json: { limit: '10b' } }).json().limit()).toEqual(10) 15 | expect(new BodyparserOptions({ json: { limit: '1kb' } }).json().limit()).toEqual(1024) 16 | }) 17 | 18 | test('contentTypes', () => { 19 | expect( 20 | new BodyparserOptions({ json: { contentTypes: [] } }).json().contentTypes() 21 | ).toEqual([]) 22 | 23 | expect( 24 | new BodyparserOptions({ 25 | json: { contentTypes: ['application/json', 'text/html'] } 26 | }).json().contentTypes() 27 | ).toEqual(['application/json', 'text/html']) 28 | 29 | expect( 30 | new BodyparserOptions({ 31 | json: { contentTypes: ['Application/JSON', 'text/HTML'] } 32 | }).json().contentTypes() 33 | ).toEqual(['application/json', 'text/html']) 34 | }) 35 | 36 | test.run() 37 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/bodyparser-multipart-options.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { BodyparserOptions } from '../../../dist/index.js' 5 | 6 | test('maxFileSize defaults to 200mb', () => { 7 | expect(new BodyparserOptions({ }).multipart().maxFileSize()).toEqual( 8 | 200 * 1024 * 1024 // 200mb in bytes 9 | ) 10 | }) 11 | 12 | test('maxFileSize', () => { 13 | expect( 14 | new BodyparserOptions({ multipart: { maxFileSize: 0 } }).multipart().maxFileSize() 15 | ).toEqual(0) 16 | 17 | expect( 18 | new BodyparserOptions({ multipart: { maxFileSize: 10 } }).multipart().maxFileSize() 19 | ).toEqual(10) 20 | 21 | expect( 22 | new BodyparserOptions({ multipart: { maxFileSize: '10kb' } }).multipart().maxFileSize() 23 | ).toEqual(10 * 1024) 24 | }) 25 | 26 | test('maxFields defaults to 1000', () => { 27 | expect(new BodyparserOptions({ }).multipart().maxFields()).toEqual(1000) 28 | }) 29 | 30 | test('maxFields', () => { 31 | expect(new BodyparserOptions({ multipart: { maxFields: 0 } }).multipart().maxFields()).toEqual(0) 32 | expect(new BodyparserOptions({ multipart: { maxFields: 10 } }).multipart().maxFields()).toEqual(10) 33 | }) 34 | 35 | test('contentTypes', () => { 36 | expect( 37 | new BodyparserOptions({ multipart: { contentTypes: [] } }).multipart().contentTypes() 38 | ).toEqual([]) 39 | 40 | expect( 41 | new BodyparserOptions({ 42 | multipart: { contentTypes: ['multipart/form-data'] } 43 | }).multipart().contentTypes() 44 | ).toEqual(['multipart/form-data']) 45 | 46 | expect( 47 | new BodyparserOptions({ 48 | multipart: { contentTypes: ['Multipart/FORM-DaTa'] } 49 | }).multipart().contentTypes() 50 | ).toEqual(['multipart/form-data']) 51 | }) 52 | 53 | test.run() 54 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/bodyparser-options.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { BodyparserOptions } from '../../../dist/index.js' 5 | 6 | test('encoding', () => { 7 | expect(new BodyparserOptions({}).encoding()).toEqual('utf8') 8 | expect(new BodyparserOptions({ encoding: 'hex' }).encoding()).toEqual('hex') 9 | }) 10 | 11 | test('methods', () => { 12 | expect(new BodyparserOptions({}).methods()).toEqual([]) 13 | 14 | expect( 15 | new BodyparserOptions({ methods: ['DELETE', 'post', 'PUT'] }).methods() 16 | ).toEqual(['DELETE', 'POST', 'PUT']) 17 | }) 18 | 19 | test.run() 20 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/bodyparser-text-options.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { BodyparserOptions } from '../../../dist/index.js' 5 | 6 | test('limit defaults to 56kb', () => { 7 | expect(new BodyparserOptions({ }).text().limit()).toEqual( 8 | 56 * 1024 // 56kb in bytes 9 | ) 10 | }) 11 | 12 | test('limit', () => { 13 | expect(new BodyparserOptions({ text: { limit: 10 } }).text().limit()).toEqual(10) 14 | expect(new BodyparserOptions({ text: { limit: '10b' } }).text().limit()).toEqual(10) 15 | expect(new BodyparserOptions({ text: { limit: '1kb' } }).text().limit()).toEqual(1024) 16 | }) 17 | 18 | test('contentTypes', () => { 19 | expect( 20 | new BodyparserOptions({ text: { contentTypes: [] } }).text().contentTypes() 21 | ).toEqual([]) 22 | 23 | expect( 24 | new BodyparserOptions({ 25 | text: { contentTypes: ['text/*'] } 26 | }).text().contentTypes() 27 | ).toEqual(['text/*']) 28 | 29 | expect( 30 | new BodyparserOptions({ 31 | text: { contentTypes: ['TeXt/*'] } 32 | }).text().contentTypes() 33 | ).toEqual(['text/*']) 34 | }) 35 | 36 | test.run() 37 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/fixtures/bodyparser-config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | encoding: 'utf-8', 4 | methods: ['POST', 'PUT', 'PATCH'], 5 | 6 | json: { 7 | limit: '1mb', 8 | contentTypes: [ 9 | 'application/json', 10 | 'application/*+json', 11 | 'application/csp-report' 12 | ] 13 | }, 14 | 15 | text: { 16 | limit: '56kb', 17 | contentTypes: ['text/*'] 18 | }, 19 | 20 | form: { 21 | limit: '56kb', 22 | contentTypes: [ 23 | 'application/x-www-form-urlencoded' 24 | ] 25 | }, 26 | 27 | multipart: { 28 | limit: '20mb', 29 | maxFields: 1000, 30 | contentTypes: [ 31 | 'multipart/form-data' 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/fixtures/test-multipart-file-1.txt: -------------------------------------------------------------------------------- 1 | This is used in the bodyparser test. 2 | -------------------------------------------------------------------------------- /packages/http/test/middleware/bodyparser/fixtures/test-multipart-file-2.txt: -------------------------------------------------------------------------------- 1 | Another file is used in the bodyparser test. 2 | -------------------------------------------------------------------------------- /packages/http/test/middleware/handle-cors/fixtures/cors-config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | maxAge: 0, 4 | 5 | allowedMethods: ['GET, HEAD, PUT, POST, DELETE, PATCH'] 6 | } 7 | -------------------------------------------------------------------------------- /packages/http/test/middleware/handle-cors/handle-cors.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Supertest from 'supertest' 4 | import { setupApp } from '../../helpers/index.js' 5 | import defaultCorsConfig from './fixtures/cors-config.js' 6 | import { HandleCorsMiddleware, Server } from '../../../dist/index.js' 7 | 8 | const app = setupApp({ cors: defaultCorsConfig }) 9 | 10 | async function createHttpServer () { 11 | const server = app.make(Server) 12 | .use(HandleCorsMiddleware) 13 | .use(ctx => { 14 | return ctx.response.payload('ok') 15 | }) 16 | 17 | await server.bootstrap() 18 | 19 | return server.callback() 20 | } 21 | 22 | test('expects 204 on preflight request', async () => { 23 | await Supertest(await createHttpServer()) 24 | .options('/') 25 | .set('Origin', 'https://superchargejs.com') 26 | .set('Access-Control-Request-Method', 'PUT') 27 | .expect(204) 28 | .expect('Access-Control-Allow-Origin', 'https://superchargejs.com') 29 | .expect('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, DELETE, PATCH') 30 | }) 31 | 32 | test('always set `vary` response header to origin', async () => { 33 | await Supertest(await createHttpServer()) 34 | .get('/') 35 | .expect(200, 'ok') 36 | .expect('Vary', 'Origin') 37 | }) 38 | 39 | test.run() 40 | -------------------------------------------------------------------------------- /packages/http/test/middleware/serve-static-assets/fixtures/public/index.html: -------------------------------------------------------------------------------- 1 |

Hello Supercharge

2 | -------------------------------------------------------------------------------- /packages/http/test/middleware/serve-static-assets/fixtures/public/style.css: -------------------------------------------------------------------------------- 1 | p { color: #ff9933; } 2 | -------------------------------------------------------------------------------- /packages/http/test/middleware/serve-static-assets/fixtures/static-assets.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | maxage: 123, 4 | 5 | defer: false, 6 | 7 | hidden: false, 8 | 9 | index: false 10 | } 11 | -------------------------------------------------------------------------------- /packages/http/test/middleware/serve-static-assets/serve-static-assets.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import Path from 'node:path' 4 | import Supertest from 'supertest' 5 | import { fileURLToPath } from 'node:url' 6 | import { setupApp } from '../../helpers/index.js' 7 | import defaultStaticAssetsConfig from './fixtures/static-assets.js' 8 | import { ServeStaticAssetsMiddleware, Server } from '../../../dist/index.js' 9 | 10 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 11 | const fixturesPath = Path.resolve(__dirname, './fixtures') 12 | 13 | const app = setupApp({ 14 | static: defaultStaticAssetsConfig, 15 | appRoot: fixturesPath 16 | }) 17 | 18 | async function createHttpServer () { 19 | const server = app.forgetInstance(Server).make(Server) 20 | .use(ServeStaticAssetsMiddleware) 21 | .use(async (ctx, next) => { 22 | if (ctx.request.path() === '/unavailable.txt') { 23 | return ctx.response.payload('ok') 24 | } 25 | 26 | await next() 27 | }) 28 | 29 | server.router().get('/:matchAll', ({ response }) => { 30 | return response.getPayload() || 'route handler response' 31 | }) 32 | 33 | await server.bootstrap() 34 | 35 | return server.callback() 36 | } 37 | 38 | test('returns index.html', async () => { 39 | await Supertest(await createHttpServer()) 40 | .get('/index.html') 41 | .expect(200, '

Hello Supercharge

\n') 42 | }) 43 | 44 | test('returns style.css', async () => { 45 | await Supertest(await createHttpServer()) 46 | .get('/style.css') 47 | .expect(200, 'p { color: #ff9933; }\n') 48 | }) 49 | 50 | test('pass through when unable to lookup asset', async () => { 51 | await Supertest(await createHttpServer()) 52 | .get('/unavailable.txt') 53 | .expect(200, 'ok') 54 | }) 55 | 56 | test.run() 57 | -------------------------------------------------------------------------------- /packages/http/test/route.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { Route } from '../dist/index.js' 5 | import { setupApp } from './helpers/index.js' 6 | 7 | let app = setupApp() 8 | 9 | test.before.each(() => { 10 | app = setupApp() 11 | }) 12 | 13 | test('fails for route controller class without .handle() method', async () => { 14 | class NoopController { } 15 | 16 | const route = new Route(['GET'], '/name', NoopController, app) 17 | await expect( 18 | route.run({}) 19 | ).rejects.toThrow('You must implement the "handle" method in controller "NoopController"') 20 | }) 21 | 22 | test('fails to run route handler for non-function/non-class handler', async () => { 23 | const route = new Route(['GET'], '/name', 'RouteController', app) 24 | await expect( 25 | route.run({}) 26 | ).rejects.toThrow('Invalid route handler. Only controller actions and inline handlers are allowed') 27 | }) 28 | 29 | test.run() 30 | -------------------------------------------------------------------------------- /packages/http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "lib": [ 7 | "ES2020", 8 | "DOM.Iterable", 9 | "DOM" 10 | ] 11 | }, 12 | "include": [ 13 | "./**/*" 14 | ], 15 | "exclude": [ 16 | "dist", 17 | "node_modules" 18 | ], 19 | "references": [ 20 | { 21 | "path": "../contracts" 22 | }, 23 | { 24 | "path": "../support" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/logging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/logging", 3 | "description": "The Supercharge logging package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js", 17 | "./drivers/file": "./dist/file-logger.js", 18 | "./drivers/console": "./dist/console-logger.js" 19 | }, 20 | "dependencies": { 21 | "@supercharge/contracts": "^4.0.0-alpha.2", 22 | "@supercharge/manager": "^4.0.0-alpha.2", 23 | "@supercharge/support": "^4.0.0-alpha.2", 24 | "chalk": "~5.3.0", 25 | "winston": "~3.11.0" 26 | }, 27 | "devDependencies": { 28 | "@supercharge/fs": "~3.4.0", 29 | "c8": "~9.1.0", 30 | "expect": "~29.7.0", 31 | "sinon": "~17.0.1", 32 | "typescript": "~5.4.5", 33 | "uvu": "~0.5.6" 34 | }, 35 | "engines": { 36 | "node": ">=22" 37 | }, 38 | "keywords": [ 39 | "supercharge", 40 | "superchargejs", 41 | "logging", 42 | "nodejs" 43 | ], 44 | "license": "MIT", 45 | "publishConfig": { 46 | "access": "public" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "directory": "packages/logging", 51 | "url": "git+https://github.com/supercharge/framework.git" 52 | }, 53 | "scripts": { 54 | "build": "tsc --build tsconfig.json --force", 55 | "dev": "npm run build -- --watch", 56 | "watch": "npm run dev", 57 | "lint": "eslint src --ext .js,.ts", 58 | "lint:fix": "npm run lint -- --fix", 59 | "test": "npm run build && npm run lint && npm run test:run", 60 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 61 | "posttest": "c8 report --reporter=html" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/logging/src/contracts.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Logger } from '@supercharge/contracts' 3 | 4 | /** 5 | * Add container bindings for services from this provider. 6 | */ 7 | declare module '@supercharge/contracts' { 8 | export interface ContainerBindings { 9 | 'logger': Logger 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/logging/src/file-logger.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Logger } from './logger.js' 3 | import Winston, { format } from 'winston' 4 | import { FileTransportInstance } from 'winston/lib/winston/transports' 5 | import { FileChannelConfig, Logger as LoggingContract } from '@supercharge/contracts' 6 | 7 | const { combine, timestamp, printf, splat } = format 8 | 9 | export class FileLogger extends Logger implements LoggingContract { 10 | /** 11 | * The log file path. 12 | */ 13 | private readonly path: string 14 | 15 | /** 16 | * Create a new file logger instance. 17 | */ 18 | constructor (options: FileChannelConfig = {}) { 19 | super(options) 20 | 21 | this.path = this.resolveLogFilePath(options.path) 22 | 23 | this.addFileTransportToLogger() 24 | } 25 | 26 | /** 27 | * Ensure the given file logger `options` contain a log file path. 28 | */ 29 | resolveLogFilePath (path?: string): string { 30 | if (!path) { 31 | throw new Error('Missing log file path when logging to a file') 32 | } 33 | 34 | return path 35 | } 36 | 37 | /** 38 | * Append a file transport to the logger instance. 39 | */ 40 | addFileTransportToLogger (): void { 41 | this.logger.add( 42 | this.createFileTransport() 43 | ) 44 | } 45 | 46 | /** 47 | * Create a file transport channel. 48 | */ 49 | createFileTransport (): FileTransportInstance { 50 | return new Winston.transports.File({ 51 | level: this.logLevel(), 52 | filename: this.path, 53 | format: combine( 54 | splat(), 55 | timestamp(), 56 | printf(info => { 57 | return JSON.stringify( 58 | Object.assign(info, { time: new Date(info.timestamp).getTime() }) 59 | ) 60 | }) 61 | ) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/logging/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { LogManager } from './log-manager.js' 3 | export { LoggingServiceProvider } from './logging-service-provider.js' 4 | -------------------------------------------------------------------------------- /packages/logging/src/logging-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { LogManager } from './log-manager.js' 3 | import { ServiceProvider } from '@supercharge/support' 4 | import type { LoggingConfig, ServiceProvider as ServiceProviderContract } from '@supercharge/contracts' 5 | 6 | export class LoggingServiceProvider extends ServiceProvider implements ServiceProviderContract { 7 | /** 8 | * Register the logger into the container. 9 | */ 10 | override register (): void { 11 | this.app().singleton('logger', () => { 12 | const loggingConfig = this.app().config().get('logging', { driver: 'console', channels: {} }) 13 | 14 | return new LogManager(this.app(), loggingConfig) 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/logging/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "exclude": [ 8 | "./node_modules", 9 | "./dist" 10 | ], 11 | "references": [ 12 | { 13 | "path": "../contracts" 14 | }, 15 | { 16 | "path": "../manager" 17 | }, 18 | { 19 | "path": "../support" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/manager", 3 | "description": "The Supercharge manager package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/strings": "~2.0.0" 21 | }, 22 | "devDependencies": { 23 | "c8": "~9.1.0", 24 | "expect": "~29.7.0", 25 | "sinon": "~17.0.1", 26 | "typescript": "~5.4.5", 27 | "uvu": "~0.5.6" 28 | }, 29 | "engines": { 30 | "node": ">=22" 31 | }, 32 | "keywords": [ 33 | "supercharge", 34 | "superchargejs", 35 | "manager", 36 | "nodejs" 37 | ], 38 | "license": "MIT", 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "directory": "packages/manager", 45 | "url": "git+https://github.com/supercharge/framework.git" 46 | }, 47 | "scripts": { 48 | "build": "tsc --build tsconfig.json --force", 49 | "dev": "npm run build -- --watch", 50 | "watch": "npm run dev", 51 | "lint": "eslint src --ext .js,.ts", 52 | "lint:fix": "npm run lint -- --fix", 53 | "test": "npm run build && npm run lint && npm run test:run", 54 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 55 | "posttest": "c8 report --reporter=html" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/manager/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { Manager } from './manager.js' 3 | -------------------------------------------------------------------------------- /packages/manager/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "exclude": [ 8 | "./node_modules", 9 | "./dist" 10 | ], 11 | "references": [ 12 | { 13 | "path": "../contracts", 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/session/src/drivers/cookie.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InteractsWithTime } from '@supercharge/support' 3 | import { SessionDriver, HttpContext } from '@supercharge/contracts' 4 | 5 | interface SessionEntry { expires: number, data: any } 6 | 7 | export class CookieSessionDriver extends InteractsWithTime implements SessionDriver { 8 | /** 9 | * Stores the HTTP context instance. 10 | */ 11 | private readonly ctx: HttpContext 12 | 13 | /** 14 | * Stores the session lifetime in seconds. 15 | */ 16 | private readonly lifetimeInSeconds: number 17 | 18 | /** 19 | * Create a new cookie session driver instance. 20 | */ 21 | constructor (lifetimeInSeconds: number, ctx: HttpContext) { 22 | super() 23 | 24 | this.ctx = ctx 25 | this.lifetimeInSeconds = lifetimeInSeconds 26 | } 27 | 28 | /** 29 | * Read the session data. 30 | */ 31 | async read (sessionId: string): Promise> { 32 | const value = this.ctx.request.cookie(sessionId) ?? '' 33 | 34 | if (!value) { 35 | return {} 36 | } 37 | 38 | const { data, expires } = JSON.parse(value) as SessionEntry 39 | 40 | if (this.now().getTime() <= expires) { 41 | return data 42 | } 43 | 44 | return {} 45 | } 46 | 47 | /** 48 | * Store the session data. 49 | */ 50 | async write (sessionId: string, data: Record): Promise { 51 | const value = JSON.stringify({ 52 | data, 53 | expires: this.availableAt(this.lifetimeInSeconds) 54 | }) 55 | 56 | this.ctx.response.cookies().set(sessionId, value) 57 | 58 | return this 59 | } 60 | 61 | /** 62 | * Delete the session data for the given `sessionId`. 63 | */ 64 | async destroy (sessionId: string): Promise { 65 | this.ctx.response.cookies().delete(sessionId) 66 | 67 | return this 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/session/src/drivers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { CookieSessionDriver } from './cookie.js' 3 | export { FileSessionDriver } from './file.js' 4 | export { MemorySessionDriver } from './memory.js' 5 | -------------------------------------------------------------------------------- /packages/session/src/drivers/memory.ts: -------------------------------------------------------------------------------- 1 | 2 | import { SessionDriver } from '@supercharge/contracts' 3 | import { InteractsWithTime } from '@supercharge/support' 4 | 5 | interface SessionEntry { expires: number, data: any } 6 | 7 | export class MemorySessionDriver extends InteractsWithTime implements SessionDriver { 8 | /** 9 | * Stores the session data. 10 | */ 11 | private readonly sessions: Map 12 | 13 | /** 14 | * Stores the session lifetime in seconds. 15 | */ 16 | private readonly lifetimeInSeconds: number 17 | 18 | /** 19 | * Create a new memory session driver instance. 20 | */ 21 | constructor (lifetimeInSeconds: number) { 22 | super() 23 | 24 | this.lifetimeInSeconds = lifetimeInSeconds 25 | this.sessions = new Map() 26 | } 27 | 28 | /** 29 | * Read the session data. 30 | */ 31 | async read (sessionId: string): Promise> { 32 | const session = this.sessions.get(sessionId) 33 | 34 | if (!session) { 35 | return {} 36 | } 37 | 38 | if (this.now().getTime() <= session.expires) { 39 | return session.data 40 | } 41 | 42 | return {} 43 | } 44 | 45 | /** 46 | * Store the session data. 47 | */ 48 | async write (sessionId: string, data: Record): Promise { 49 | this.sessions.set(sessionId, { 50 | data, 51 | expires: this.availableAt(this.lifetimeInSeconds) 52 | }) 53 | 54 | return this 55 | } 56 | 57 | /** 58 | * Delete the session data for the given `sessionId`. 59 | */ 60 | async destroy (sessionId: string): Promise { 61 | this.sessions.delete(sessionId) 62 | 63 | return this 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/session/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { CookieSessionDriver, FileSessionDriver, MemorySessionDriver } from './drivers/index.js' 3 | export { StartSessionMiddleware, VerifyCsrfTokenMiddleware } from './middleware/index.js' 4 | export { Session } from './session.js' 5 | export { SessionConfig } from './session-config.js' 6 | export { SessionManager } from './session-manager.js' 7 | export { SessionServiceProvider } from './session-service-provider.js' 8 | -------------------------------------------------------------------------------- /packages/session/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './start-session.js' 3 | export * from './verify-csrf-token.js' 4 | -------------------------------------------------------------------------------- /packages/session/test/helpers/error-handler.js: -------------------------------------------------------------------------------- 1 | 2 | import { ErrorHandler as Handler } from '@supercharge/core' 3 | 4 | export default class ErrorHandler extends Handler { 5 | handle (error, ctx) { 6 | // console.error('Received error in testing error handler', { error }) 7 | 8 | return super.handle(error, ctx) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/session/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import ErrorHandler from './error-handler.js' 5 | import { Application } from '@supercharge/core' 6 | import { SessionServiceProvider } from '../../dist/index.js' 7 | 8 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 9 | const appRootPath = Path.resolve(__dirname, './fixtures') 10 | 11 | /** 12 | * Returns a test application. 13 | * 14 | * @param {import('@supercharge/contracts').SessionConfig} sessionConfig 15 | * 16 | * @returns {Promise} 17 | */ 18 | export async function setupApp (sessionConfig = {}) { 19 | const app = Application 20 | .createWithAppRoot(appRootPath) 21 | .withErrorHandler(ErrorHandler) 22 | .bind('view', () => viewMock) 23 | 24 | app.config() 25 | .set('app.key', 'app-key-1234') 26 | .set('bodyparser', { 27 | methods: ['POST'] 28 | }) 29 | .set('http', { 30 | host: 'localhost', 31 | port: 1234 32 | }) 33 | .set('session', { 34 | driver: 'cookie', 35 | name: 'supercharge-test-session', 36 | lifetime: 60, 37 | ...sessionConfig 38 | }) 39 | 40 | await app 41 | .register(new SessionServiceProvider(app)) 42 | .boot() 43 | 44 | return app 45 | } 46 | 47 | const viewMock = { 48 | render () { 49 | return '

error-view

' 50 | }, 51 | exists (view) { 52 | return view === 'errors/401' 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/session/test/session-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { Server } from '@supercharge/http' 5 | import { setupApp } from './helpers/index.js' 6 | import { Application } from '@supercharge/core' 7 | import { SessionServiceProvider, SessionManager, StartSessionMiddleware } from '../dist/index.js' 8 | 9 | test('throws without session config', async () => { 10 | const app = new Application() 11 | app.register(new SessionServiceProvider(app)) 12 | 13 | expect(() => { 14 | app.make('session') 15 | }).toThrow('Missing session configuration file. Make sure the "config/session.ts" file exists.') 16 | }) 17 | 18 | test('register session service provider', async () => { 19 | const app = await setupApp() 20 | 21 | expect(app.make('session') instanceof SessionManager).toBe(true) 22 | }) 23 | 24 | test('registers the StartSession middleware', async () => { 25 | const app = await setupApp() 26 | const server = app.make(Server) 27 | 28 | server.app().hasBinding(StartSessionMiddleware) 29 | }) 30 | 31 | test('boot the registered session service provider', async () => { 32 | const app = await setupApp() 33 | 34 | expect(app.make('session') instanceof SessionManager).toBe(true) 35 | 36 | const Request = app.make('request') 37 | expect(Request.hasMacro('session')).toBe(true) 38 | expect(Request.prototype.session).toBeDefined() 39 | }) 40 | 41 | test.run() 42 | -------------------------------------------------------------------------------- /packages/session/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "lib": [ 7 | "ES2020", 8 | "DOM.Iterable", 9 | "DOM" 10 | ] 11 | }, 12 | "include": [ 13 | "./**/*" 14 | ], 15 | "exclude": [ 16 | "dist", 17 | "node_modules" 18 | ], 19 | "references": [ 20 | { 21 | "path": "../contracts" 22 | }, 23 | { 24 | "path": "../manager" 25 | }, 26 | { 27 | "path": "../support" 28 | }, 29 | { 30 | "path": "../core" 31 | }, 32 | { 33 | "path": "../http" 34 | }, 35 | { 36 | "path": "../view" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /packages/support/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/support", 3 | "description": "The Supercharge support package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/contracts": "^4.0.0-alpha.2", 20 | "@supercharge/goodies": "~2.0.0" 21 | }, 22 | "devDependencies": { 23 | "c8": "~9.1.0", 24 | "dayjs": "~1.11.10", 25 | "expect": "~29.7.0", 26 | "typescript": "~5.4.5", 27 | "uvu": "~0.5.6" 28 | }, 29 | "engines": { 30 | "node": ">=22" 31 | }, 32 | "homepage": "https://superchargejs.com", 33 | "keywords": [ 34 | "supercharge", 35 | "superchargejs", 36 | "support", 37 | "service-provider", 38 | "nodejs" 39 | ], 40 | "license": "MIT", 41 | "publishConfig": { 42 | "access": "public" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "directory": "packages/support", 47 | "url": "git+https://github.com/supercharge/framework.git" 48 | }, 49 | "scripts": { 50 | "build": "tsc --build tsconfig.json --force", 51 | "dev": "npm run build -- --watch", 52 | "watch": "npm run dev", 53 | "lint": "eslint src --ext .js,.ts", 54 | "lint:fix": "npm run lint -- --fix", 55 | "test": "npm run build && npm run lint && npm run test:run", 56 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 57 | "posttest": "c8 report --reporter=html" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/support/src/html-string.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Htmlable } from '@supercharge/contracts' 3 | 4 | export class HtmlString implements Htmlable { 5 | /** 6 | * Stores the HTML string. 7 | */ 8 | private readonly html: string 9 | 10 | constructor (html?: string) { 11 | this.html = html ?? '' 12 | } 13 | 14 | /** 15 | * Create an HtmlString instance for the given `html`. 16 | * 17 | * @return {HtmlString} 18 | */ 19 | static from (html?: string): HtmlString { 20 | return new this(html) 21 | } 22 | 23 | /** 24 | * Returns the content as an HTML string. 25 | * 26 | * @return string 27 | */ 28 | toString (): string { 29 | return this.toHtml() 30 | } 31 | 32 | /** 33 | * Returns the content as an HTML string. 34 | * 35 | * @return string 36 | */ 37 | toHtml (): string { 38 | return this.html 39 | } 40 | 41 | /** 42 | * Determine whether the given HTML string is empty. 43 | */ 44 | isEmpty (): boolean { 45 | return this.html.length === 0 46 | } 47 | 48 | /** 49 | * Determine whether the given HTML string is not empty. 50 | */ 51 | isNotEmpty (): boolean { 52 | return !this.isEmpty() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/support/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { HtmlString } from './html-string.js' 3 | export { InteractsWithTime } from './interacts-with-time.js' 4 | export { ServiceProvider } from './service-provider.js' 5 | -------------------------------------------------------------------------------- /packages/support/src/interacts-with-time.ts: -------------------------------------------------------------------------------- 1 | 2 | export class InteractsWithTime { 3 | /** 4 | * Returns the current time as a date instance. 5 | */ 6 | protected now (): Date { 7 | return new Date() 8 | } 9 | 10 | /** 11 | * Returns the current time as a UNIX timestamp. 12 | */ 13 | protected currentTime (): number { 14 | return Math.floor( 15 | this.now().getTime() / 1000 16 | ) 17 | } 18 | 19 | /** 20 | * Add the given number of `seconds` to the `date`. 21 | */ 22 | protected addSecondsDelay (date: Date, seconds: number): Date { 23 | const delayed = new Date(date) 24 | delayed.setSeconds(date.getSeconds() + seconds) 25 | 26 | return delayed 27 | } 28 | 29 | /** 30 | * Returns the "available at" UNIX timestamp with the added `delay` in seconds. 31 | */ 32 | protected availableAt (delay: Date | number): number { 33 | if (delay instanceof Date) { 34 | return delay.getTime() 35 | } 36 | 37 | return this.addSecondsDelay(this.now(), delay).getTime() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/support/test/fixtures/test-config-without-default-export.js: -------------------------------------------------------------------------------- 1 | 2 | export const config = { 3 | testing: true 4 | } 5 | -------------------------------------------------------------------------------- /packages/support/test/fixtures/test-config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | testing: true 4 | } 5 | -------------------------------------------------------------------------------- /packages/support/test/html-string.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { HtmlString } from '../dist/index.js' 5 | 6 | test('static from', () => { 7 | const str = '

Hello Supercharge

' 8 | const html = HtmlString.from(str) 9 | expect(str).toEqual(html.toHtml()) 10 | }) 11 | 12 | test('toHtml', () => { 13 | const str = '

Hello Supercharge

' 14 | const html = new HtmlString(str) 15 | expect(str).toEqual(html.toHtml()) 16 | }) 17 | 18 | test('toString', () => { 19 | const str = '

Hello Supercharge

' 20 | const html = new HtmlString(str) 21 | 22 | expect(str).toEqual(html.toString()) 23 | expect(str).toEqual(String(html)) 24 | }) 25 | 26 | test('isEmpty', () => { 27 | const html = new HtmlString() 28 | 29 | expect(html.isEmpty()).toBe(true) 30 | expect(html.isNotEmpty()).toBe(false) 31 | 32 | expect(new HtmlString('').isEmpty()).toBe(true) 33 | expect(new HtmlString(null).isEmpty()).toBe(true) 34 | }) 35 | 36 | test.run() 37 | -------------------------------------------------------------------------------- /packages/support/test/interacts-with-time.js: -------------------------------------------------------------------------------- 1 | 2 | import Dayjs from 'dayjs' 3 | import { test } from 'uvu' 4 | import { expect } from 'expect' 5 | import { promisify } from 'node:util' 6 | import ChildProcess from 'node:child_process' 7 | import { InteractsWithTime } from '../dist/index.js' 8 | 9 | const exec = promisify(ChildProcess.exec) 10 | 11 | test('currentTime', async () => { 12 | const time = new InteractsWithTime() 13 | const { stdout: unixTimestamp } = await exec('date +%s') 14 | 15 | expect(time.currentTime()).toBeLessThan(Number(unixTimestamp) + 3) 16 | expect(time.currentTime()).toBeGreaterThan(Number(unixTimestamp) - 3) 17 | }) 18 | 19 | test('availableAt', async () => { 20 | const now = new Date() 21 | const time = new InteractsWithTime() 22 | 23 | expect(time.availableAt(now)).toEqual(now.getTime()) 24 | 25 | const expected = Dayjs().add(60, 'seconds').toDate().getTime() 26 | expect(time.availableAt(60)).toBeGreaterThan(expected - 12) 27 | expect(time.availableAt(60)).toBeLessThanOrEqual(expected + 12) 28 | }) 29 | 30 | test.run() 31 | -------------------------------------------------------------------------------- /packages/support/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": [ 8 | "./**/*" 9 | ], 10 | "exclude": [ 11 | "dist", 12 | "node_modules" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../contracts" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supercharge/view", 3 | "description": "The Supercharge view package", 4 | "version": "4.0.0-alpha.2", 5 | "author": "Marcus Pöhls ", 6 | "bugs": { 7 | "url": "https://github.com/supercharge/framework/issues" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "type": "module", 13 | "main": "dist/index.js", 14 | "types": "dist", 15 | "exports": { 16 | ".": "./dist/index.js" 17 | }, 18 | "dependencies": { 19 | "@supercharge/collections": "~5.0.1", 20 | "@supercharge/contracts": "^4.0.0-alpha.2", 21 | "@supercharge/fs": "~3.4.0", 22 | "@supercharge/goodies": "~2.0.0", 23 | "@supercharge/manager": "^4.0.0-alpha.2", 24 | "@supercharge/strings": "~2.0.0", 25 | "@supercharge/support": "^4.0.0-alpha.2", 26 | "handlebars": "~4.7.8" 27 | }, 28 | "devDependencies": { 29 | "@supercharge/application": "^4.0.0-alpha.2", 30 | "c8": "~9.1.0", 31 | "expect": "~29.7.0", 32 | "typescript": "~5.4.5", 33 | "uvu": "~0.5.6" 34 | }, 35 | "engines": { 36 | "node": ">=22" 37 | }, 38 | "homepage": "https://superchargejs.com", 39 | "keywords": [ 40 | "supercharge", 41 | "superchargejs", 42 | "view", 43 | "render", 44 | "view-rendering", 45 | "nodejs" 46 | ], 47 | "license": "MIT", 48 | "publishConfig": { 49 | "access": "public" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "directory": "packages/view", 54 | "url": "git+https://github.com/supercharge/framework.git" 55 | }, 56 | "scripts": { 57 | "build": "tsc --build tsconfig.json --force", 58 | "dev": "npm run build -- --watch", 59 | "watch": "npm run dev", 60 | "lint": "eslint src --ext .js,.ts", 61 | "lint:fix": "npm run lint -- --fix", 62 | "test": "npm run build && npm run lint && npm run test:run", 63 | "test:run": "c8 --include=dist uvu --ignore helpers --ignore fixtures", 64 | "posttest": "c8 report --reporter=html" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/view/src/drivers/base-driver.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ViewSharedData } from '@supercharge/contracts' 3 | 4 | export class ViewBaseDriver { 5 | /** 6 | * Stores the data that is available to all view templates. 7 | */ 8 | private state: Record = {} 9 | 10 | /** 11 | * Share a given state of data to all views, across HTTP requests. 12 | * 13 | * @example 14 | * ``` 15 | * import { View } from '@supercharge/facades' 16 | * 17 | * View.share({ key: 'value' }) 18 | * ``` 19 | */ 20 | share (key: K, value: ViewSharedData[K]): this 21 | share (values: Partial): this 22 | share (key: string | any, value?: any): this { 23 | if (this.isObject(key)) { 24 | this.state = { ...this.state, ...key } 25 | } else if (typeof key === 'string') { 26 | this.state[key] = value 27 | } else { 28 | throw new Error(`Failed to set shared view state: the first argument is neither a string nor an object. Received "${typeof key}"`) 29 | } 30 | 31 | return this 32 | } 33 | 34 | /** 35 | * Returns the shared data. 36 | */ 37 | sharedData (): Record { 38 | return this.state 39 | } 40 | 41 | /** 42 | * Determine whether the given `input` is an object. 43 | */ 44 | private isObject (input: any): input is Record { 45 | return !!input && input.constructor.name === 'Object' 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/helpers/append.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Push content to the end of an assets stack identified by the given 4 | * `stackName`. Check out the `stack` helper for more details. 5 | */ 6 | export default function append (stackName: string, context: any): void { 7 | if (!context) { 8 | throw new Error('Provide a name when using the "append" handlebars helper.') 9 | } 10 | 11 | context.data.root = context.data.root ?? {} 12 | const stacks = context.data.root.stacks ?? {} 13 | const stack = stacks[stackName] ?? [] 14 | 15 | // @ts-expect-error 16 | stack.push({ mode: 'append', data: context.fn(this) }) 17 | 18 | context.data.root.stacks = { ...stacks, [stackName]: stack } 19 | } 20 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/helpers/json.ts: -------------------------------------------------------------------------------- 1 | 2 | import Handlebars, { SafeString } from 'handlebars' 3 | 4 | /** 5 | * Creates a JSON string from the content parameter. 6 | * 7 | * @example 8 | * ```hbs 9 | * {{json user}} 10 | * renders 11 | * {"name": "Supercharge"} 12 | * 13 | * {{json user pretty=true}} 14 | * renders with line breaks and 2-space indention 15 | * { 16 | * "name": "Supercharge" 17 | * } 18 | * ``` 19 | */ 20 | export default function json (content: string, options: any): SafeString { 21 | return new Handlebars.SafeString( 22 | options.hash.pretty 23 | ? JSON.stringify(content, undefined, 2) 24 | : JSON.stringify(content) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/helpers/prepend.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Push content to the beginning of an assets stack identified by the 4 | * given `stackName`. Check out the `stack` helper for more details. 5 | */ 6 | export default function prepend (stackName: string, context: any): void { 7 | if (!context) { 8 | throw new Error('Provide a name when using the "prepend" handlebars helper.') 9 | } 10 | 11 | context.data.root = context.data.root ?? {} 12 | const stacks = context.data.root.stacks ?? {} 13 | const stack = stacks[stackName] ?? [] 14 | 15 | // @ts-expect-error 16 | stack.unshift({ mode: 'prepend', data: context.fn(this) }) 17 | 18 | context.data.root.stacks = { ...stacks, [stackName]: stack } 19 | } 20 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/helpers/raw.ts: -------------------------------------------------------------------------------- 1 | 2 | import { TemplateDelegate } from 'handlebars' 3 | 4 | /** 5 | * Raw block helper for templates that need 6 | * to handle unprocessed mustache blocks. 7 | * 8 | * For example, Vue.js uses mustache templates 9 | * and Handlebars would render the Vue.js tags 10 | * before Vue can pick them up. 11 | * 12 | * @example 13 | * ``` 14 | * {{{{raw}}}} 15 | * {{bar}} 16 | * {{{{/raw}}}} 17 | * ``` 18 | * 19 | * will render 20 | * 21 | * ``` 22 | * {{bar}} 23 | * ``` 24 | * Find more details in the Handlebars documentation: 25 | * https://handlebarsjs.com/block_helpers.html#raw-blocks 26 | * 27 | */ 28 | export default function raw (options: any): TemplateDelegate { 29 | return options.fn() 30 | } 31 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/helpers/stack.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Stacks are like portals and allow to inject content into a section 4 | * in a layout or view from a different view (like a partial). 5 | * 6 | * @example 7 | * ``` 8 | * {{#stack "scripts"}} 9 | * … this is the default content of this stack 10 | * {{/stack}} 11 | * ``` 12 | */ 13 | export default function stack (name: string, context: any): string { 14 | if (!context) { 15 | throw new Error('Provide a name when using the "stack" handlebars helper.') 16 | } 17 | 18 | const data = context.data.root ?? {} 19 | const stacks = data.stacks ?? {} 20 | const stack = stacks[name] ?? [] 21 | 22 | const content = stack 23 | .reduce((carry: string[], { mode, data }: { mode: string, data: string }) => { 24 | if (mode === 'append') { 25 | carry.push(data) 26 | } 27 | 28 | if (mode === 'prepend') { 29 | carry.unshift(data) 30 | } 31 | 32 | return carry 33 | // @ts-expect-error 34 | }, [context.fn(this)]) 35 | .join('') 36 | 37 | return content 38 | } 39 | -------------------------------------------------------------------------------- /packages/view/src/drivers/handlebars/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './handlebars-driver.js' 3 | -------------------------------------------------------------------------------- /packages/view/src/drivers/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './handlebars/index.js' 3 | -------------------------------------------------------------------------------- /packages/view/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { HandlebarsDriver } from './drivers/index.js' 3 | export { ViewConfigBuilder } from './view-config-builder.js' 4 | export { ViewManager } from './view-manager.js' 5 | export { ViewResponse } from './view-response.js' 6 | export { ViewServiceProvider } from './view-service-provider.js' 7 | -------------------------------------------------------------------------------- /packages/view/src/view-config-builder.ts: -------------------------------------------------------------------------------- 1 | 2 | import { tap } from '@supercharge/goodies' 3 | import { ViewConfigBuilder as ViewConfigBuilderContract, ViewResponseConfig } from '@supercharge/contracts' 4 | 5 | export class ViewConfigBuilder implements ViewConfigBuilderContract { 6 | /** 7 | * Stores the view config. 8 | */ 9 | private readonly config: ViewResponseConfig 10 | 11 | /** 12 | * Create a new view config builder instance. 13 | */ 14 | constructor (config: ViewResponseConfig) { 15 | this.config = config 16 | } 17 | 18 | /** 19 | * Create a new view config builder instance. 20 | */ 21 | static from (config: ViewResponseConfig): ViewConfigBuilder { 22 | return new this(config) 23 | } 24 | 25 | /** 26 | * Set the base layout used to render this view. The given `name` identifies 27 | * the file name of the layout file in the configured layouts folder. 28 | */ 29 | layout (name: string): this { 30 | return tap(this, () => { 31 | this.config.layout = name 32 | }) 33 | } 34 | 35 | /** 36 | * Render this view without a base layout. 37 | */ 38 | withoutLayout (): this { 39 | return this.layout('') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/view/src/view-response.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ViewConfigBuilder } from './view-config-builder.js' 3 | import { HttpResponse, ViewBuilderCallback, ViewEngine, ViewResponseConfig } from '@supercharge/contracts' 4 | 5 | export class ViewResponse { 6 | /** 7 | * Stores the HTTP response instance. 8 | */ 9 | private readonly response: HttpResponse 10 | 11 | /** 12 | * Stores the view engine instance. 13 | */ 14 | private readonly viewEngine: ViewEngine 15 | 16 | /** 17 | * Create a new view manager instance. 18 | */ 19 | constructor (response: HttpResponse, viewEngine: ViewEngine) { 20 | this.response = response 21 | this.viewEngine = viewEngine 22 | } 23 | 24 | /** 25 | * Render a view template as the response. 26 | */ 27 | async render (template: string, dataOrViewBuilder?: ViewBuilderCallback | any): Promise 28 | async render (template: string, data?: any, viewBuilder?: ViewBuilderCallback): Promise 29 | async render (template: string, data?: any, viewBuilder?: ViewBuilderCallback): Promise { 30 | if (typeof data === 'function') { 31 | viewBuilder = data 32 | data = {} 33 | } 34 | 35 | return this.response.payload( 36 | await this.renderView(template, data, viewBuilder) 37 | ) 38 | } 39 | 40 | /** 41 | * Assigns the rendered HTML of the given `template` as the response payload. 42 | */ 43 | private async renderView (template: string, data?: any, viewBuilderCallback?: ViewBuilderCallback): Promise { 44 | const viewData = { 45 | ...this.response.state().all(), 46 | ...this.viewEngine.sharedData(), 47 | ...data 48 | } 49 | 50 | const viewConfig: ViewResponseConfig = {} 51 | 52 | if (typeof viewBuilderCallback === 'function') { 53 | viewBuilderCallback( 54 | ViewConfigBuilder.from(viewConfig) 55 | ) 56 | } 57 | 58 | return await this.viewEngine.render(template, viewData, viewConfig) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/helpers/test-helper.js: -------------------------------------------------------------------------------- 1 | 2 | export default function testHelper () {} 3 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/layouts/test.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{ content }}} 3 |
4 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/partials/test-partial.hbs: -------------------------------------------------------------------------------- 1 |

Test Partial

2 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-helper-json-pretty.hbs: -------------------------------------------------------------------------------- 1 | {{json user pretty=true}} 2 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-helper-json.hbs: -------------------------------------------------------------------------------- 1 |

{{json user}}

2 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-helper-raw.hbs: -------------------------------------------------------------------------------- 1 | {{{{raw}}}} 2 | {{name}} 3 | {{{{/raw}}}} 4 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-helper-stack-append-prepend.hbs: -------------------------------------------------------------------------------- 1 | {{#append "test-stack"}} 2 | append 3 | {{/append}} 4 | 5 | {{#prepend "test-stack"}} 6 | prepend 7 | {{/prepend}} 8 | 9 | {{#stack "test-stack"}} 10 | content 11 | {{/stack}} 12 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-helper-stack.hbs: -------------------------------------------------------------------------------- 1 | {{#stack 'test-stack'}} 2 |

part of test-stack

3 | {{/stack}} 4 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-with-in-memory-helper.hbs: -------------------------------------------------------------------------------- 1 |

Test View: {{test}}

2 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-with-in-memory-partial.hbs: -------------------------------------------------------------------------------- 1 |

Test View: {{> test}}

2 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view-with-partial.hbs: -------------------------------------------------------------------------------- 1 |

Test View: {{ name }}

2 | {{> test-partial }} 3 | -------------------------------------------------------------------------------- /packages/view/test/fixtures/resources/views/test-view.hbs: -------------------------------------------------------------------------------- 1 |

Test View: {{name}}

2 | -------------------------------------------------------------------------------- /packages/view/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { Application } from '@supercharge/application' 5 | import { ViewServiceProvider } from '../../dist/index.js' 6 | 7 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 8 | const appRoot = Path.resolve(__dirname, './../fixtures') 9 | 10 | /** 11 | * Returns a test application. 12 | * 13 | * @returns {Application} 14 | */ 15 | export function makeApp () { 16 | const app = Application.createWithAppRoot(appRoot) 17 | 18 | app.config().set('view', { 19 | driver: 'handlebars', 20 | handlebars: { 21 | views: app.resourcePath('views'), 22 | partials: app.resourcePath('views/partials'), 23 | helpers: app.resourcePath('views/helpers'), 24 | layouts: app.resourcePath('views/layouts') 25 | // defaultLayout: 'app' 26 | } 27 | }) 28 | 29 | app.register(new ViewServiceProvider(app)) 30 | 31 | return app 32 | } 33 | -------------------------------------------------------------------------------- /packages/view/test/view-helper.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { makeApp } from './helpers/index.js' 5 | 6 | test('renders raw helper', async () => { 7 | const app = makeApp() 8 | const view = app.make('view') 9 | await view.boot() 10 | 11 | expect( 12 | await view.render('test-view-helper-raw') 13 | ).toEqual('{{name}}\n') 14 | }) 15 | 16 | test('renders stack helper', async () => { 17 | const app = makeApp() 18 | const view = app.make('view') 19 | await view.boot() 20 | 21 | expect( 22 | await view.render('test-view-helper-stack') 23 | ).toEqual('

part of test-stack

\n') 24 | }) 25 | 26 | test('renders stack, append, prepend helpers', async () => { 27 | const app = makeApp() 28 | const view = app.make('view') 29 | await view.boot() 30 | 31 | expect( 32 | await view.render('test-view-helper-stack-append-prepend') 33 | ).toEqual('\n\n prepend\n content\n append\n') 34 | }) 35 | 36 | test('json', async () => { 37 | const app = makeApp() 38 | const view = app.make('view') 39 | await view.boot() 40 | 41 | const data = { user: { name: 'Supercharge' } } 42 | expect( 43 | await view.render('test-view-helper-json', data) 44 | ).toEqual(`

${JSON.stringify(data.user)}

\n`) 45 | }) 46 | 47 | test('json prettry', async () => { 48 | const app = makeApp() 49 | const view = app.make('view') 50 | await view.boot() 51 | 52 | const data = { user: { name: 'Supercharge' } } 53 | expect( 54 | await view.render('test-view-helper-json-pretty', data) 55 | ).toEqual(`${JSON.stringify(data.user, null, 2)}\n`) 56 | }) 57 | 58 | test.run() 59 | -------------------------------------------------------------------------------- /packages/view/test/view-service-provider.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { makeApp } from './helpers/index.js' 5 | import { Application } from '@supercharge/application' 6 | import { ViewServiceProvider, ViewManager } from '../dist/index.js' 7 | 8 | test('throws without view config', async () => { 9 | const app = new Application() 10 | app.register(new ViewServiceProvider(app)) 11 | 12 | expect(() => { 13 | app.make('view') 14 | }).toThrow('Missing view configuration file. Make sure the "config/view.ts" file exists.') 15 | }) 16 | 17 | test('register view service provider', async () => { 18 | const app = makeApp() 19 | app.register(new ViewServiceProvider(app)) 20 | 21 | expect(app.make('view') instanceof ViewManager).toBe(true) 22 | }) 23 | 24 | test('boot the registered view service provider', async () => { 25 | const app = makeApp() 26 | 27 | await app 28 | .register(new ViewServiceProvider(app)) 29 | .bind('response', () => { 30 | return { 31 | macro () { } 32 | } 33 | }) 34 | .boot() 35 | 36 | expect(app.make('view') instanceof ViewManager).toBe(true) 37 | }) 38 | 39 | test.run() 40 | -------------------------------------------------------------------------------- /packages/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "exclude": [ 8 | "dist", 9 | "node_modules" 10 | ], 11 | "references": [ 12 | { 13 | "path": "../application" 14 | }, 15 | { 16 | "path": "../contracts" 17 | }, 18 | { 19 | "path": "../manager" 20 | }, 21 | { 22 | "path": "../support" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/vite/src/backend/vite-manifest.ts: -------------------------------------------------------------------------------- 1 | 2 | import Fs from 'node:fs' 3 | import { Manifest, ManifestChunk } from 'vite' 4 | 5 | export class ViteManifest { 6 | /** 7 | * Stores the Vite manifest object. 8 | */ 9 | private readonly manifest: Manifest 10 | 11 | /** 12 | * Create a new instance. 13 | */ 14 | constructor (manifest: Manifest) { 15 | this.manifest = manifest 16 | } 17 | 18 | /** 19 | * Load a Vite manifest file from the given `manifestPath`. 20 | */ 21 | static loadFrom (manifestPath: string): ViteManifest { 22 | this.ensureManifestExists(manifestPath) 23 | 24 | const manifest = JSON.parse( 25 | Fs.readFileSync(manifestPath, 'utf8') 26 | ) 27 | 28 | return new this(manifest) 29 | } 30 | 31 | /** 32 | * Ensure the Vite manifest file exists. 33 | */ 34 | static ensureManifestExists (manifestPath: string): void { 35 | if (!Fs.existsSync(manifestPath)) { 36 | throw new Error(`Vite manifest file not found at path: ${manifestPath} `) 37 | } 38 | } 39 | 40 | /** 41 | * Determine whether the given `entrypoint` exists in the manifest. 42 | */ 43 | hasEntrypoint (entrypoint: string): boolean { 44 | try { 45 | this.getChunk(entrypoint) 46 | return true 47 | } catch (error) { 48 | return false 49 | } 50 | } 51 | 52 | /** 53 | * Returns the manifest chunk for the given `entrypoint`. Throws an error if no chunk exists. 54 | */ 55 | getChunk (entrypoint: string): ManifestChunk { 56 | const chunk = this.manifest[entrypoint] 57 | 58 | if (!chunk) { 59 | throw new Error(`Entrypoint not found in manifest: "${entrypoint}"`) 60 | } 61 | 62 | return chunk 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/vite/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { supercharge } from './plugin/plugin.js' 3 | 4 | export default supercharge 5 | export { supercharge } 6 | 7 | export { Vite, ViteTagAttributes } from './backend/vite.js' 8 | export { ViteConfig } from './backend/vite-config.js' 9 | export { ViteServiceProvider } from './vite-service-provider.js' 10 | 11 | export { resolvePageComponent } from './inertia/inertia-helpers.js' 12 | export { InertiaPageNotFoundError } from './inertia/inertia-page-not-found-error.js' 13 | 14 | export { HotReloadFile } from './plugin/hotfile.js' 15 | export { PluginConfigContract, DevServerUrl } from './plugin/types.js' 16 | -------------------------------------------------------------------------------- /packages/vite/src/inertia/inertia-helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InertiaPageNotFoundError } from './inertia-page-not-found-error.js' 3 | 4 | /** 5 | * Resolves the inertia page component for the given `path` from the available `pages`. 6 | * 7 | * @example 8 | * ```js 9 | * createInertiaApp({ 10 | * resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**\/*.vue')), 11 | * setup({ el, App, props, plugin }) { 12 | * … 13 | * } 14 | * }); 15 | * ``` 16 | */ 17 | export async function resolvePageComponent (path: string, pages: Record | (() => Promise)>): Promise { 18 | const page = pages[path] 19 | 20 | if (page == null) { 21 | throw new InertiaPageNotFoundError(path) 22 | } 23 | 24 | return typeof page === 'function' 25 | ? await page() 26 | : page 27 | } 28 | -------------------------------------------------------------------------------- /packages/vite/src/inertia/inertia-page-not-found-error.ts: -------------------------------------------------------------------------------- 1 | 2 | export class InertiaPageNotFoundError extends Error { 3 | /** 4 | * Create a new instance. 5 | */ 6 | constructor (path: string) { 7 | super(`Inertia page not found: ${path}`) 8 | 9 | this.name = 'InertiaPageNotFoundError' 10 | InertiaPageNotFoundError.captureStackTrace(this, this.constructor) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/vite/src/plugin/hotfile.ts: -------------------------------------------------------------------------------- 1 | 2 | import Fs from 'node:fs' 3 | import Path from 'node:path' 4 | import { HotReloadFileContent } from './types.js' 5 | 6 | export class HotReloadFile { 7 | /** 8 | * Stores the Vite config object. 9 | */ 10 | private readonly hotfilePath: string 11 | 12 | /** 13 | * Create a new instance. 14 | */ 15 | constructor (hotfilePath: string) { 16 | this.hotfilePath = hotfilePath 17 | this.deleteHotfileOnProcessExit() 18 | } 19 | 20 | /** 21 | * Returns a new instance for the given hot-reload file path. 22 | */ 23 | static from (hotfilePath: string): HotReloadFile { 24 | return new this(hotfilePath) 25 | } 26 | 27 | /** 28 | * Clean-up the hot-reload file when exiting the process. 29 | */ 30 | private deleteHotfileOnProcessExit (): void { 31 | process.on('exit', () => this.deleteHotfile()) 32 | process.on('SIGINT', process.exit) 33 | process.on('SIGHUP', process.exit) 34 | process.on('SIGTERM', process.exit) 35 | } 36 | 37 | /** 38 | * Delete the hot-reload file from disk. 39 | */ 40 | deleteHotfile (): void { 41 | if (Fs.existsSync(this.hotfilePath)) { 42 | Fs.rmSync(this.hotfilePath) 43 | } 44 | } 45 | 46 | /** 47 | * Write the hot-reload file to disk. 48 | */ 49 | writeFileSync (content: HotReloadFileContent): void { 50 | Fs.mkdirSync(Path.dirname(this.hotfilePath), { recursive: true }) 51 | Fs.writeFileSync(this.hotfilePath, JSON.stringify(content)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/vite/src/plugin/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface HotReloadFileContent { 3 | viteDevServerUrl: string 4 | } 5 | 6 | export type DevServerUrl = `${'http' | 'https'}://${string}:${number}` 7 | 8 | export interface PluginConfigContract { 9 | /** 10 | * The path or paths to the entrypoints to compile with Vite. 11 | */ 12 | input: string | string[] 13 | 14 | /** 15 | * The "public" directory name. 16 | * 17 | * @default "public" 18 | */ 19 | publicDirectory?: string 20 | 21 | /** 22 | * The "build" subdirectory name where compiled assets should be written. 23 | * 24 | * @default "build" 25 | */ 26 | buildDirectory?: string 27 | 28 | /** 29 | * The SSR entry point path(s). 30 | */ 31 | ssr?: string | string[] 32 | 33 | /** 34 | * The hot-reload file path. 35 | * 36 | * @default "${publicDirectory}/'hot')" 37 | */ 38 | hotFilePath?: string 39 | 40 | /** 41 | * The directory where the SSR bundle should be written. 42 | * 43 | * @default 'bootstrap/ssr' 44 | */ 45 | ssrOutputDirectory?: string 46 | } 47 | -------------------------------------------------------------------------------- /packages/vite/src/vite-service-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Vite } from './backend/vite.js' 3 | import { ViteConfig } from './backend/vite-config.js' 4 | import { ServiceProvider } from '@supercharge/support' 5 | import { ViteHandlebarsHelper } from './backend/vite-handlebars-helper.js' 6 | import { ViewEngine, ViteConfig as ViteConfigContract } from '@supercharge/contracts' 7 | 8 | /** 9 | * Add container bindings for services from this provider. 10 | */ 11 | declare module '@supercharge/contracts' { 12 | export interface ContainerBindings { 13 | 'vite': Vite 14 | } 15 | } 16 | 17 | export class ViteServiceProvider extends ServiceProvider { 18 | /** 19 | * Register application services. 20 | */ 21 | override async boot (): Promise { 22 | this.registerVite() 23 | this.registerViteViewHelpers() 24 | } 25 | 26 | /** 27 | * Register the Vite binding. 28 | */ 29 | private registerVite (): void { 30 | this.app().singleton('vite', () => { 31 | const config = this.app().config().get('vite') 32 | const viteConfig = ViteConfig.from(config) 33 | 34 | return Vite.from(viteConfig) 35 | }) 36 | } 37 | 38 | /** 39 | * Register the Vite view helper. 40 | */ 41 | private registerViteViewHelpers (): void { 42 | const vite = this.app().make('vite') 43 | const view = this.app().make('view') 44 | 45 | view.registerHelper('vite', (...args: any[]) => { 46 | return new ViteHandlebarsHelper(vite, ...args).generateTags() 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vite/test/fixtures/resources/views/test-vite-helper-hash-arguments-number.hbs: -------------------------------------------------------------------------------- 1 | {{{ vite input=123 }}} 2 | -------------------------------------------------------------------------------- /packages/vite/test/fixtures/resources/views/test-vite-helper-hash-arguments.hbs: -------------------------------------------------------------------------------- 1 | {{{ vite input="resources/js/hash-app.js, resources/css/app.css" }}} 2 | -------------------------------------------------------------------------------- /packages/vite/test/fixtures/resources/views/test-vite-helper-unnamed-arguments.hbs: -------------------------------------------------------------------------------- 1 | {{{ vite "resources/js/app.js" "resources/css/app.css" }}} 2 | -------------------------------------------------------------------------------- /packages/vite/test/fixtures/resources/views/test-vite-helper-with-attributes.hbs: -------------------------------------------------------------------------------- 1 | {{{ vite input="resources/js/hash-app.js, resources/css/app.css" attributes='data-turbo-track="reload" async' }}} 2 | -------------------------------------------------------------------------------- /packages/vite/test/fixtures/test-page.js: -------------------------------------------------------------------------------- 1 | export default 'Dummy File' 2 | -------------------------------------------------------------------------------- /packages/vite/test/inertia-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | import { test } from 'uvu' 3 | import { expect } from 'expect' 4 | import { resolvePageComponent, InertiaPageNotFoundError } from '../dist/index.js' 5 | 6 | const testPage = './fixtures/test-page.js' 7 | 8 | test('pass glob value to resolvePageComponent', async () => { 9 | const file = await resolvePageComponent(testPage, import.meta.glob('./fixtures/*.js')) 10 | expect(file.default).toBe('Dummy File') 11 | }) 12 | 13 | test('pass eagerly globed value to resolvePageComponent', async () => { 14 | const file = await resolvePageComponent(testPage, import.meta.glob('./fixtures/*.js', { eager: true })) 15 | expect(file.default).toBe('Dummy File') 16 | }) 17 | 18 | test('fails for non-existing page', async () => { 19 | await expect( 20 | resolvePageComponent('./fixtures/not-existing.js', import.meta.glob('./fixtures/*.js')) 21 | ).rejects.toThrowError(InertiaPageNotFoundError) 22 | 23 | await expect( 24 | resolvePageComponent('./fixtures/not-existing.js', import.meta.glob('./fixtures/*.js')) 25 | ).rejects.toThrow('Inertia page not found: ./fixtures/not-existing.js') 26 | }) 27 | 28 | test.run() 29 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "lib": [ 7 | "ES2020", 8 | "DOM.Iterable", 9 | "DOM" 10 | ] 11 | }, 12 | "include": [ 13 | "./**/*" 14 | ], 15 | "exclude": [ 16 | "dist", 17 | "node_modules" 18 | ], 19 | "references": [ 20 | { 21 | "path": "../contracts" 22 | }, 23 | { 24 | "path": "../facades" 25 | }, 26 | { 27 | "path": "../http" 28 | }, 29 | { 30 | "path": "../support" 31 | }, 32 | { 33 | "path": "../view" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supercharge/tsconfig", 3 | "compilerOptions": { 4 | "composite": true, 5 | "removeComments": false, 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | "dist" 10 | ] 11 | } 12 | --------------------------------------------------------------------------------