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"]
+}