diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e291365 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..00334f2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,63 @@ +const { resolve } = require('path'); + +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: [resolve(__dirname, './tsconfig.json')], + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + ignorePatterns: [ + '.eslintrc.js', + 'index.js', + '*.config.js', + '*.config.ts', + 'build.constants.ts', + 'scripts/*', + '**/*.cjs.js', + '**/*.cjs.prod.js', + '**/*.esm-bundler.js', + '**/*.esm-browser.js', + '**/*.esm-browser.prod.js', + '**/*.global.js', + '**/*.global.prod.js', + '**/dist/*.d.ts', + 'explorations/*' + ], + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier', + ], + root: true, + env: { + node: true, + jest: true, + }, + rules: { + 'indent': ['error', 4, { 'SwitchCase': 1 }], + 'comma-dangle': ['error', 'always-multiline'], + 'no-multiple-empty-lines': ['error', { 'max': 1 }], + 'lines-between-class-members': ['error', 'always'], + 'padded-blocks': ['error', 'never'], + 'eol-last': ['error', 'always'], + 'quotes': ['error', 'single'], + 'semi': ['error', 'never'], + 'eol-last': ['error', 'always'], + + // TypeScript + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-empty-interface': 'off', + // '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-implied-eval': 'off', + + '@typescript-eslint/no-this-alias': 'warn', + + // Allow debugger during development only + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + }, +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04de573 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +explorations +package-lock.json +dist +temp +coverage \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6da93d4 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Express Adapter (Wooks Composables) + +**!!! This is work-in-progress library, breaking changes are expected !!!** + +

+
+ + + +

+ +Want to use [Wooks Composables](https://github.com/wooksjs/composables) but your project is coupled with express? āœ… This is not a problem with this Express Adapter for [Wooks Composables](https://github.com/wooksjs/composables) + +šŸ”„ Get power of [Wooks Composables](https://github.com/wooksjs/composables) in your express project! + +## Install + +`npm install @wooksjs/express-adapter` + +## Usage + +```ts +import { applyExpressAdapter } from '@wooksjs/express-adapter' +import { useBody } from '@wooksjs/body' +import { useRouteParams, WooksError } from '@wooksjs/composables' + +const app = express() + +applyExpressAdapter(app) + +app.get('/test/:param', () => { + const { getRouteParam } = useRouteParams() + return { message: 'it works', param: getRouteParam('param') } +}) + +app.post('/post', () => { + const { parseBody } = useBody() + return parseBody() +}) + +app.get('/error', () => { + throw new WooksError(400, 'test error') +}) + +app.listen(3000, () => console.log('listening 3000')) +``` diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000..e55d58d --- /dev/null +++ b/build.config.ts @@ -0,0 +1,17 @@ +import { defineBuildConfig } from 'unbuild' +import constants from './build.constants' + +export default defineBuildConfig({ + declaration: true, + rollup: { + emitCJS: true, + inlineDependencies: true, + replace: { + values: constants, + preventAssignment: true + }, + }, + entries: [ + 'src/index' + ], + }) \ No newline at end of file diff --git a/build.constants.ts b/build.constants.ts new file mode 100644 index 0000000..2bb3263 --- /dev/null +++ b/build.constants.ts @@ -0,0 +1,36 @@ +import pkg from './package.json' +import { dye, TDyeColor, TDyeModifier } from '@prostojs/dye' +const dyeModifiers: TDyeModifier[] = ['dim', 'bold', 'underscore', 'inverse', 'italic', 'crossed'] +const dyeColors: TDyeColor[] = ['red', 'green', 'cyan', 'blue', 'yellow', 'white', 'magenta', 'black'] + +function createDyeReplaceConst() { + const c = dye('red') + const bg = dye('bg-red') + const dyeReplacements = { + '__DYE_RESET__': '\'' + dye.reset + '\'', + '__DYE_COLOR_OFF__': '\'' + c.close + '\'', + '__DYE_BG_OFF__': '\'' + bg.close + '\'', + } + dyeModifiers.forEach(v => { + dyeReplacements[`__DYE_${ v.toUpperCase() }__`] = '\'' + dye(v).open + '\'' + dyeReplacements[`__DYE_${ v.toUpperCase() }_OFF__`] = '\'' + dye(v).close + '\'' + }) + dyeColors.forEach(v => { + dyeReplacements[`__DYE_${ v.toUpperCase() }__`] = '\'' + dye(v).open + '\'' + dyeReplacements[`__DYE_BG_${ v.toUpperCase() }__`] = '\'' + dye(('bg-' + v) as TDyeColor).open + '\'' + dyeReplacements[`__DYE_${ v.toUpperCase() }_BRIGHT__`] = '\'' + dye((v + '-bright') as TDyeColor).open + '\'' + dyeReplacements[`__DYE_BG_${ v.toUpperCase() }_BRIGHT__`] = '\'' + dye(('bg-' + v + '-bright') as TDyeColor).open + '\'' + }) + return dyeReplacements +} + +function createProjectReplaceConst() { + return { + __VERSION__: `'${pkg.version}'`, + } +} + +export default { + ...createDyeReplaceConst(), + ...createProjectReplaceConst(), +} diff --git a/docs/icon.png b/docs/icon.png new file mode 100644 index 0000000..ec8fc07 Binary files /dev/null and b/docs/icon.png differ diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4526c50 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,35 @@ +const { dye } = require('@prostojs/dye') + +module.exports = { + preset: 'ts-jest', + moduleFileExtensions: [ + "ts", + "js" + ], + rootDir: __dirname, + testRegex: ".spec.ts$", + transform: { + "^.+\\.(t|j)s$": "ts-jest" + }, + coverageDirectory: 'coverage', + coverageReporters: ['html', 'lcov', 'text'], + collectCoverageFrom: [ + 'src/**/*.ts', + ], + watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'], + testEnvironment: "node", + globals: { + __DYE_RED_BRIGHT__: dye('red-bright').open, + __DYE_BOLD__: dye('bold').open, + __DYE_RESET__: dye.reset, + __DYE_RED__: dye('red').open, + __DYE_COLOR_OFF__: dye('red').close, + __DYE_GREEN__: dye('green').open, + __DYE_GREEN_BRIGHT__: dye('green-bright').open, + __DYE_BLUE__: dye('blue').open, + __DYE_YELLOW__: dye('yellow').open, + __DYE_DIM__: dye('dim').open, + __DYE_DIM_OFF__: dye('dim').close, + __VERSION__: 'JEST_TEST', + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f43e47 --- /dev/null +++ b/package.json @@ -0,0 +1,90 @@ +{ + "name": "@wooksjs/express-adapter", + "version": "0.0.0", + "description": "Express Adapter for Wooks Composables", + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "release": "node ./scripts/release", + "test": "jest --runInBand", + "test:cov": "jest --runInBand --coverage", + "lint": "eslint --ext .ts src/**/**.ts", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", + "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wooksjs/express-adapter.git" + }, + "exports": { + ".": { + "node": { + "require": "./dist/index.cjs", + "import": "./dist/index.mjs" + } + } + }, + "keywords": [ + "http", + "wooks", + "composables", + "express", + "adapter", + "web", + "framework", + "app", + "api", + "rest", + "restful", + "prostojs" + ], + "buildOptions": { + "formats": [ + "esm-bundler", + "cjs" + ] + }, + "gitHooks": { + "commit-msg": "node scripts/verifyCommit.js" + }, + "author": "Artem Maltsev", + "license": "MIT", + "bugs": { + "url": "https://github.com/wooksjs/express-adapter/issues" + }, + "homepage": "https://github.com/wooksjs/express-adapter#readme", + "peerDependencies": { + "@wooksjs/composables": "^0.0.1-alpha.8", + "express": "^4.0.0" + }, + "devDependencies": { + "@prostojs/dye": "^0.3.0", + "@types/express": "^4.17.14", + "@types/jest": "^29.2.0", + "@types/node": "^18.11.0", + "@typescript-eslint/eslint-plugin": "^5.42.0", + "@typescript-eslint/parser": "^5.0.0", + "@wooksjs/composables": "^0.0.1-alpha.8", + "conventional-changelog": "^3.1.24", + "conventional-changelog-cli": "^2.1.1", + "enquirer": "^2.3.6", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.24.2", + "execa": "^5.1.1", + "express": "^4.18.2", + "jest": "^29.2.2", + "minimist": "^1.2.6", + "semver": "^7.3.5", + "ts-jest": "^29.0.3", + "tslib": "^2.4.1", + "typescript": "^4.8.4", + "unbuild": "^0.9.4", + "yorkie": "^2.0.0" + } +} diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 0000000..e3cbef9 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,130 @@ +const args = require('minimist')(process.argv.slice(2)) +const path = require('path') +const execa = require('execa') +const { prompt } = require('enquirer') +const version = require('../package.json').version +const semver = require('semver') +const { dye } = require('@prostojs/dye') +const run = (bin, args, opts = {}) => + execa(bin, args, { stdio: 'inherit', ...opts }) +const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name) + +const step = dye('cyan').prefix('\n').attachConsole() +const error = dye('red-bright').attachConsole('error') +const good = dye('green', 'bold').prefix('\nāœ“ ').attachConsole() +const info = dye('green', 'dim').attachConsole('info') + +const branch = execa.sync('git', ['branch', '--show-current']).stdout +const inc = i => { + if (['prerelease', 'premajor'].includes(i.split(' ')[0])) { + const [action, pre] = i.split(' ') + return semver.inc(version, action, pre) + } else { + return semver.inc(version, i) + } +} + +const isDryRun = args.dry +const skipTests = args.skipTests +const skipBuild = args.skipBuild + +const commitMessage = execa.sync('git', ['log', '-1', '--pretty=%B']).stdout + +const gitStatus = execa.sync('git', ['status']).stdout +if (gitStatus.indexOf('nothing to commit, working tree clean') < 0) { + error('Please commit all the changes first.') + process.exit(1) +} + +main() + +async function main() { + let targetVersion = version + if (branch === 'main') { + // for main proposing typeof version increase + const versionIncrements = [ + 'patch', + 'minor', + 'prerelease alpha', + 'prerelease beta', + 'preminor alpha', + 'preminor beta', + 'premajor alpha', + 'premajor beta', + 'major', + ] + + const { release } = await prompt({ + type: 'select', + name: 'release', + message: 'Select release type', + choices: versionIncrements.map(i => `${i} (${inc(i)})`) + }) + + + targetVersion = release.match(/\((.*)\)/)[1] + + if (!semver.valid(targetVersion)) { + throw new Error(`invalid target version: ${targetVersion}`) + } + + const { yes } = await prompt({ + type: 'confirm', + name: 'yes', + message: `Releasing v${targetVersion}. Confirm?` + }) + + if (!yes) { + return + } + + // run tests before release + step('Running tests...') + if (!skipTests && !isDryRun) { + await run(bin('jest'), ['--clearCache']) + await run('npm', ['test', '--', '--bail']) + } else { + info(`(skipped)`) + } + + step('Running lint...') + if (!skipTests && !isDryRun) { + await run('npm', ['run', 'lint']) + } else { + info(`(skipped)`) + } + + // build all packages with types + step('Building package...') + if (!skipBuild && !isDryRun) { + await run('npm', ['run', 'build', '--', '--release']) + } else { + info(`(skipped)`) + } + + const npmAction = release.split(' ')[0] + const pre = release.split(' ')[1] + const preAction = [ + 'prerelease', + 'preminor', + 'premajor', + ].includes(npmAction) ? ['--preid', pre] : [] + + step('Creating a new version ' + targetVersion + ' ...') + execa.sync('npm', ['version', npmAction, ...preAction, '-m', commitMessage]) + + } else { + error('Branch "main" expected') + } + + step('Pushing changes ...') + execa.sync('git', ['push']) + + step('Pushing tags ...') + execa.sync('git', ['push', '--tags']) + + step('Publishing ...') + execa.sync('npm', ['publish', '--access', 'public']) + + good('All done!') +} diff --git a/scripts/verifyCommit.js b/scripts/verifyCommit.js new file mode 100644 index 0000000..c56e876 --- /dev/null +++ b/scripts/verifyCommit.js @@ -0,0 +1,33 @@ +const { dye } = require('@prostojs/dye') + +// Invoked on the commit-msg git hook by yorkie. +const msgPath = process.env.GIT_PARAMS +const msg = require('fs') + .readFileSync(msgPath, 'utf-8') + .trim() + +const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/ + +const s = { + error: dye('white', 'bg-red', 'bold'), + errorText: dye('red'), + green: dye('green'), +} + +if (!commitRE.test(msg)) { + console.log() + console.error( + ` ${s.error(' ERROR ')} ${s.errorText( + `invalid commit message format.` + )}\n\n` + + s.errorText( + ` Proper commit message format is required for automated changelog generation. Examples:\n\n` + ) + + ` ${s.green(`feat(compiler): add 'comments' option`)}\n` + + ` ${s.green( + `fix(v-model): handle events on blur (close #28)` + )}\n\n` + + s.errorText(` See .github/commit-convention.md for more details.\n`) + ) + process.exit(1) +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..1ca8415 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,61 @@ +/* eslint-disable no-var */ +// Global compile-time constants +declare var __DEV__: boolean +declare var __TEST__: boolean +declare var __BROWSER__: boolean +declare var __GLOBAL__: boolean +declare var __ESM_BUNDLER__: boolean +declare var __ESM_BROWSER__: boolean +declare var __NODE_JS__: boolean +declare var __SSR__: boolean +declare var __COMMIT__: string +declare var __VERSION__: string + +// dye colors +declare var __DYE_RESET__: string +declare var __DYE_COLOR_OFF__: string +declare var __DYE_BG_OFF__: string +declare var __DYE_DIM__: string +declare var __DYE_DIM_OFF__: string +declare var __DYE_BOLD__: string +declare var __DYE_BOLD_OFF__: string +declare var __DYE_UNDERSCORE__: string +declare var __DYE_UNDERSCORE_OFF__: string +declare var __DYE_INVERSE__: string +declare var __DYE_INVERSE_OFF__: string +declare var __DYE_ITALIC__: string +declare var __DYE_ITALIC_OFF__: string +declare var __DYE_CROSSED__: string +declare var __DYE_CROSSED_OFF__: string +declare var __DYE_RED__: string +declare var __DYE_BG_RED__: string +declare var __DYE_RED_BRIGHT__: string +declare var __DYE_BG_RED_BRIGHT__: string +declare var __DYE_GREEN__: string +declare var __DYE_BG_GREEN__: string +declare var __DYE_GREEN_BRIGHT__: string +declare var __DYE_BG_GREEN_BRIGHT__: string +declare var __DYE_CYAN__: string +declare var __DYE_BG_CYAN__: string +declare var __DYE_CYAN_BRIGHT__: string +declare var __DYE_BG_CYAN_BRIGHT__: string +declare var __DYE_BLUE__: string +declare var __DYE_BG_BLUE__: string +declare var __DYE_BLUE_BRIGHT__: string +declare var __DYE_BG_BLUE_BRIGHT__: string +declare var __DYE_YELLOW__: string +declare var __DYE_BG_YELLOW__: string +declare var __DYE_YELLOW_BRIGHT__: string +declare var __DYE_BG_YELLOW_BRIGHT__: string +declare var __DYE_WHITE__: string +declare var __DYE_BG_WHITE__: string +declare var __DYE_WHITE_BRIGHT__: string +declare var __DYE_BG_WHITE_BRIGHT__: string +declare var __DYE_MAGENTA__: string +declare var __DYE_BG_MAGENTA__: string +declare var __DYE_MAGENTA_BRIGHT__: string +declare var __DYE_BG_MAGENTA_BRIGHT__: string +declare var __DYE_BLACK__: string +declare var __DYE_BG_BLACK__: string +declare var __DYE_BLACK_BRIGHT__: string +declare var __DYE_BG_BLACK_BRIGHT__: string diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f1a0b90 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { createWooksCtx, createWooksResponder, useWooksCtx } from '@wooksjs/composables' +import { Express } from 'express' +import { IncomingMessage, ServerResponse } from 'http' + +const methods = [ + 'get', 'post', 'delete', 'patch', 'options', +] + +export function applyExpressAdapter(app: Express) { + const responder = createWooksResponder() + + function useWooksDecorator(fn: () => unknown) { + return async () => { + const { restoreCtx, clearCtx } = useWooksCtx() + try { + const result = await fn() + restoreCtx() + responder.respond(result) + } catch (e) { + responder.respond(e) + } + clearCtx() + } + } + + app.use(wooksContext) + + for (const m of methods) { + const defFn: (...args: any[]) => void = (app[m as keyof Express] as (...args: any[]) => void).bind(app) + const newFn: (...args: any[]) => void = ((...args: Parameters) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return + return defFn(...args.map(a => typeof a === 'function' ? useWooksDecorator(a as (() => unknown)) : a)) + }).bind(app) + Object.defineProperty(app, m, { value: newFn }) + } +} + +function wooksContext(req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) { + createWooksCtx({ req, res }) + next() +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8e31962 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext", + "DOM", + "WebWorker", + "DOM.Iterable" + ], + "module": "ESNext", + "declaration": true, + "sourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "rootDir": ".", + "allowJs": true, + "outDir": "build", + "downlevelIteration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true, + "noUnusedLocals": false, + "noLib": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "types": ["jest", "node"] + }, + "include": ["src/**/*", "**/*.spec.ts", "**/*.types.ts"], + "exclude": ["node_modules", "explorations", "scripts"] +}