├── .github └── workflows │ └── .ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── page-express-mapper.js ├── sampleApp ├── sampleApp.html └── sampleApp.js ├── test └── test.js └── webpack.config.js /.github/workflows/.ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: CI 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | node-version: [18.x, 20.x, 22.x] 9 | os: [ubuntu-latest, macos-latest, windows-latest] 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm ci 17 | - run: npm run lint 18 | - run: npm run coverage 19 | - run: npm run codecov 20 | env: 21 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Linux ### 2 | *~ 3 | 4 | # temporary files which can be created if a process still has a handle open of a deleted file 5 | .fuse_hidden* 6 | 7 | # KDE directory preferences 8 | .directory 9 | 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | 13 | # .nfs files are created when an open file is removed but is still being accessed 14 | .nfs* 15 | 16 | ### macOS ### 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | 22 | # Icon must end with two \r 23 | Icon 24 | 25 | # Thumbnails 26 | ._* 27 | 28 | # Files that might appear in the root of a volume 29 | .DocumentRevisions-V100 30 | .fseventsd 31 | .Spotlight-V100 32 | .TemporaryItems 33 | .Trashes 34 | .VolumeIcon.icns 35 | .com.apple.timemachine.donotpresent 36 | 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | ### Node ### 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | lerna-debug.log* 52 | 53 | # Diagnostic reports (https://nodejs.org/api/report.html) 54 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | *.pid.lock 61 | 62 | # Directory for instrumented libs generated by jscoverage/JSCover 63 | lib-cov 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | *.lcov 68 | 69 | # nyc test coverage 70 | .nyc_output 71 | 72 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 73 | .grunt 74 | 75 | # Bower dependency directory (https://bower.io/) 76 | bower_components 77 | 78 | # node-waf configuration 79 | .lock-wscript 80 | 81 | # Compiled binary addons (https://nodejs.org/api/addons.html) 82 | build/Release 83 | 84 | # Dependency directories 85 | node_modules/ 86 | jspm_packages/ 87 | 88 | # TypeScript v1 declaration files 89 | typings/ 90 | 91 | # TypeScript cache 92 | *.tsbuildinfo 93 | 94 | # Optional npm cache directory 95 | .npm 96 | 97 | # Optional eslint cache 98 | .eslintcache 99 | 100 | # Optional REPL history 101 | .node_repl_history 102 | 103 | # Output of 'npm pack' 104 | *.tgz 105 | 106 | # Yarn Integrity file 107 | .yarn-integrity 108 | 109 | # dotenv environment variables file 110 | .env 111 | .env.test 112 | 113 | # parcel-bundler cache (https://parceljs.org/) 114 | .cache 115 | 116 | # next.js build output 117 | .next 118 | 119 | # nuxt.js build output 120 | .nuxt 121 | 122 | # rollup.js default build output 123 | dist/ 124 | 125 | # Uncomment the public line if your project uses Gatsby 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 128 | # public 129 | 130 | # Storybook build outputs 131 | .out 132 | .storybook-out 133 | 134 | # vuepress build output 135 | .vuepress/dist 136 | 137 | # Serverless directories 138 | .serverless/ 139 | 140 | # FuseBox cache 141 | .fusebox/ 142 | 143 | # DynamoDB Local files 144 | .dynamodb/ 145 | 146 | # Temporary folders 147 | tmp/ 148 | temp/ 149 | 150 | ### Windows ### 151 | # Windows thumbnail cache files 152 | Thumbs.db 153 | Thumbs.db:encryptable 154 | ehthumbs.db 155 | ehthumbs_vista.db 156 | 157 | # Dump file 158 | *.stackdump 159 | 160 | # Folder config file 161 | [Dd]esktop.ini 162 | 163 | # Recycle Bin used on file shares 164 | $RECYCLE.BIN/ 165 | 166 | # Windows Installer files 167 | *.cab 168 | *.msi 169 | *.msix 170 | *.msm 171 | *.msp 172 | 173 | # Windows shortcuts 174 | *.lnk 175 | 176 | # Sample app build artifacts 177 | sampleApp/build 178 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Next version 4 | 5 | - Put your changes here... 6 | 7 | ## 2.0.2 8 | 9 | - Added support for Express middleware (multiple chained callbacks). 10 | - Bumped various dependencies. 11 | 12 | ## 2.0.1 13 | 14 | - Added `router.stack` feature. 15 | - Bumped various dependencies. 16 | 17 | ## 2.0.0 18 | 19 | - Transferred ownership of this repo to the Roosevelt framework. 20 | - Dropped bower support, replaced it with npm distribution. 21 | - Added webpack bundling. 22 | - Removed `expressAppName` param. 23 | - Refactored code. 24 | - Rewrote sample app to reflect the overhaul. 25 | - Overhauled README to reflect the overhaul. 26 | - Added CHANGELOG and CONTRIBUTING docs. 27 | 28 | ## 1.0.6 29 | 30 | - Removed moot bower version. 31 | 32 | ## 1.0.5 33 | 34 | - Removed hard dependency: Having this dependency explicitly declared caused page.js to get downloaded twice potentially and also made it harder for users to control which version of page.js they prefer. 35 | 36 | ## 1.0.4 37 | 38 | - Fixed bower ignore issue. 39 | 40 | ## 1.0.3 41 | 42 | - page.js 1.5 support and browserify support. 43 | 44 | ## 1.0.2 45 | 46 | - page.js 1.4.0 compatibility. 47 | 48 | ## 1.0.1 49 | 50 | - Removing redundant code. 51 | 52 | ## 1.0.0 53 | 54 | - Initial version. 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Before opening a pull request 4 | 5 | - Be sure all tests pass: `npm t`. 6 | - Ensure 100% code coverage and write new tests if necessary: `npm run coverage`. 7 | - Add your changes to `CHANGELOG.md`. 8 | 9 | ## Release process 10 | 11 | If you are a maintainer of this module, please follow the following release procedure: 12 | 13 | - Merge all desired pull requests into master. 14 | - Bump `package.json` to a new version and run `npm i` to generate a new `package-lock.json`. 15 | - Alter CHANGELOG "Next version" section and stamp it with the new version. 16 | - Paste contents of CHANGELOG into new version commit. 17 | - Open and merge a pull request with those changes. 18 | - Tag the merge commit as the a new release version number. 19 | - Publish commit to npm. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | === 3 | 4 | All original code in this project is licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). Commercial and noncommercial use is permitted with attribution. 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | page-express-mapper 2 | === 3 | 4 | [![Build Status](https://github.com/rooseveltframework/page-express-mapper/workflows/CI/badge.svg 5 | )](https://github.com/rooseveltframework/page-express-mapper/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/rooseveltframework/page-express-mapper/branch/master/graph/badge.svg)](https://codecov.io/gh/rooseveltframework/page-express-mapper) [![npm](https://img.shields.io/npm/v/page-express-mapper.svg)](https://www.npmjs.com/package/page-express-mapper) 6 | 7 | **This project is no longer maintained. See [single-page-express](https://github.com/rooseveltframework/single-page-express) for a spiritual successor.** 8 | 9 | A plugin for [page.js](http://visionmedia.github.io/page.js/) which aims to provide a direct imitation of the [Express](http://expressjs.com/) API so you can write isomorphic (aka universal, [amphibious](https://twitter.com/kethinov/status/566896168324825088), etc) router code that can be shared on the client and the server with your Express application without modification. 10 | 11 | With this plugin you should be able to write isomorphic JavaScript apps that maximize code reuse on the client and the server, so long as you run Express on the server, page.js on the client, and use a JS-based templating system that can run on the client and server. 12 | 13 | This module was built and is maintained by the [Roosevelt web framework](https://github.com/rooseveltframework/roosevelt) [team](https://github.com/orgs/rooseveltframework/people), but it can be used independently of Roosevelt as well. 14 | 15 | Usage 16 | === 17 | 18 | - Add `page-express-mapper` to your npm dependencies. 19 | - Load `page-express-mapper.js` into your frontend JS bundle along with `page.js`. 20 | - Then initialize it by calling `pageExpressMapper()` *before* defining any routes: 21 | 22 | Assuming your server code begins something like: 23 | 24 | ```js 25 | // express-server.js 26 | 27 | // init express 28 | const app = express() 29 | 30 | // load an isomorphic routes.js file that declares routes 31 | // you can share this file verbatim with the client 32 | require('routes')(app) 33 | ``` 34 | 35 | And your client code begins something like: 36 | 37 | ```js 38 | // client.js 39 | 40 | // require dependencies 41 | const page = require('page') 42 | const pageExpressMapper = require('page-express-mapper') 43 | 44 | // configure pageExpressMapper 45 | const router = pageExpressMapper({ 46 | renderMethod: function(template, model) { 47 | // render a template using your 48 | // favorite templating system here 49 | } 50 | }) 51 | 52 | 53 | // load the same isomorphic routes.js file as above 54 | // you can share this file verbatim with the server 55 | require('routes')(router) 56 | 57 | // init page.js 58 | page() 59 | ``` 60 | 61 | You can then write identical routes for both sides, such as: 62 | 63 | ```js 64 | // routes.js 65 | 66 | // these routes will be shared on both the client and server 67 | module.exports = function(router) { 68 | router.route('/someRoute').get(function(req, res) { 69 | res.render('someTemplate', {some: 'model'}) 70 | }) 71 | } 72 | ``` 73 | 74 | Note: You will also need a way to share your templates with the client and server as well. If you're using [Roosevelt](https://github.com/rooseveltframework/roosevelt), you can use the `clientViews` feature. If you're using vanilla Express, you'll need to create a build script that will allow the template files to be shared on both sides. 75 | 76 | Params 77 | === 78 | 79 | `pageExpressMapper()` returns a `router` object to attach routes to (like Express) and accepts the following params: 80 | 81 | function renderMethod(template, model) 82 | --- 83 | 84 | *Required* 85 | 86 | This is designed to mimic the [Express render method](http://expressjs.com/api.html#app.render). Load your own templating system elsewhere in your app and call its render method here as a function of your own design. 87 | 88 | As with Express, the `template` argument should refer to a named template and the `model` argument should be a JSON string or JavaScript object. 89 | 90 | For example, using the [Teddy](https://github.com/rooseveltframework/teddy) templating engine: 91 | 92 | ```js 93 | pageExpressMapper({ 94 | renderMethod: function(template, model) { 95 | const newHTML = teddy.render(template, model) 96 | // do something with the new HTML 97 | } 98 | }) 99 | ``` 100 | 101 | This should work with any templating engine which supports both client-side rendering and Express on the server-side. 102 | 103 | object customRouter 104 | --- 105 | 106 | *Optional* 107 | 108 | By default this plugin matches the Express 4 API, but if you want to remap it, supply a `customRouter`. For example, to match the Express 3 API, you could do this: 109 | 110 | ```js 111 | pageExpressMapper({ 112 | renderMethod: someRenderMethod, 113 | customRouter: { 114 | get: function(route, callback) { 115 | page(route, callback) 116 | }, 117 | post: function(route, callback) { 118 | page(route, callback) 119 | }, 120 | all: function(route, callback) { 121 | page(route, callback) 122 | } 123 | } 124 | }) 125 | ``` 126 | 127 | Default: `undefined` 128 | 129 | The `router` object returned by `pageExpressMapper()` also has a member object called `stack` indexed by route with member booleans for whether the route accepts GET, POST, or both. This is useful for getting a list of all registered routes on your frontend. 130 | 131 | Example `router.stack` object: 132 | 133 | ```javascript 134 | { 135 | "/": { 136 | "get": true 137 | }, 138 | "/about": { 139 | "get": true 140 | }, 141 | "/pageWithForm": { 142 | "get": true, 143 | "post": true 144 | } 145 | } 146 | ``` 147 | 148 | Sample app 149 | === 150 | 151 | - Clone this repo. 152 | - Install dependencies. 153 | - `cd` to your clone. 154 | - `npm run sample-app`. 155 | - Open the browser dev tools and examine the console logs when you click the links in the sample app to see evidence of it working. 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "page-express-mapper", 3 | "description": "Plugin for page.js which directly imitates the Express API.", 4 | "author": "Roosevelt Framework Team ", 5 | "contributors": [ 6 | { 7 | "name": "Contributors", 8 | "url": "https://github.com/rooseveltframework/page-express-mapper/graphs/contributors" 9 | } 10 | ], 11 | "version": "2.0.2", 12 | "files": [ 13 | "dist/page-express-mapper.js" 14 | ], 15 | "homepage": "https://github.com/rooseveltframework/page-express-mapper", 16 | "license": "CC-BY-4.0", 17 | "main": "page-express-mapper.js", 18 | "readmeFilename": "README.md", 19 | "engines": { 20 | "node": ">=18.0.0" 21 | }, 22 | "dependencies": { 23 | "page": "1.11.6" 24 | }, 25 | "devDependencies": { 26 | "http-server": "14.1.1", 27 | "webpack-cli": "5.1.4", 28 | "ava": "6.1.3", 29 | "c8": "10.1.2", 30 | "codecov": "3.8.3", 31 | "eslint": "8.57.1", 32 | "eslint-plugin-ava": "14.0.0", 33 | "standard": "17.1.2" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/rooseveltframework/page-express-mapper.git" 38 | }, 39 | "keywords": [ 40 | "page.js", 41 | "express", 42 | "express-mapper", 43 | "page.js-express-mapper", 44 | "pageExpressMapper", 45 | "page" 46 | ], 47 | "eslintConfig": { 48 | "parserOptions": { 49 | "ecmaVersion": 2020 50 | }, 51 | "plugins": [ 52 | "ava" 53 | ], 54 | "rules": { 55 | "ava/no-only-test": "error" 56 | } 57 | }, 58 | "scripts": { 59 | "build": "webpack", 60 | "sample-app": "webpack && http-server -o sampleApp/sampleApp.html", 61 | "codecov": "codecov", 62 | "coverage": "c8 --reporter=text --reporter=lcov ava", 63 | "lint": "standard && eslint test/test.js", 64 | "test": "ava" 65 | }, 66 | "funding": "https://www.paypal.com/donate/?hosted_button_id=2L2X8GRXZCGJ6" 67 | } 68 | -------------------------------------------------------------------------------- /page-express-mapper.js: -------------------------------------------------------------------------------- 1 | const page = require('page') 2 | 3 | function pageExpressMapper (params) { 4 | let router, res 5 | 6 | // renderMethod param 7 | if (params.renderMethod && typeof params.renderMethod === 'function') { 8 | res = { 9 | render: params.renderMethod, // template, model 10 | redirect: function (route) { 11 | page.redirect(route) 12 | } 13 | } 14 | 15 | // overload page.js route prototype 16 | page.Route.prototype.middleware = function (fn) { 17 | const self = this 18 | return function (ctx, next) { 19 | if (self.match(ctx.path, ctx.params)) return fn(ctx, res, next) // the method is the same except this line was modified 20 | next() 21 | } 22 | } 23 | } 24 | 25 | // customRouter param 26 | if (params.customRouter) { 27 | router = params.customRouter 28 | } else { 29 | router = { 30 | route: function (route) { 31 | return { 32 | get: function (...callbacks) { 33 | page(route, ...callbacks) 34 | router.stack[route] = router.stack[route] || {} 35 | router.stack[route].get = true 36 | }, 37 | post: function (...callbacks) { 38 | page(route, ...callbacks) 39 | router.stack[route] = router.stack[route] || {} 40 | router.stack[route].post = true 41 | }, 42 | all: function (...callbacks) { 43 | page(route, ...callbacks) 44 | router.stack[route] = router.stack[route] || {} 45 | router.stack[route].get = true 46 | router.stack[route].post = true 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | router.stack = {} 54 | return router 55 | } 56 | 57 | module.exports = pageExpressMapper 58 | -------------------------------------------------------------------------------- /sampleApp/sampleApp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simple sample app to test page-express-mapper 8 | 9 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sampleApp/sampleApp.js: -------------------------------------------------------------------------------- 1 | const page = require('page') 2 | const pageExpressMapper = require('../page-express-mapper') 3 | 4 | // activate express-mapper plugin 5 | const app = pageExpressMapper({ 6 | renderMethod: function (template, model) { 7 | console.log('template supplied was: ' + template) 8 | console.log('and the model:') 9 | console.log(model) 10 | } 11 | }) 12 | 13 | // define routes 14 | app.route('/testlink1').get(function (req, res) { 15 | res.render('someTemplate', { some: 'model' }) 16 | }) 17 | app.route('/testlink2').get(function (req, res) { 18 | res.render('someOtherTemplate', { someOther: 'model' }) 19 | }) 20 | app.route('/testlink3').get(function (req, res) { 21 | res.render('templateThree', { template: 'three' }) 22 | }) 23 | 24 | // activate router 25 | page() 26 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | test('TODO: tests', async t => { 4 | t.true('TODO: tests'.includes('tests')) 5 | }) 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = [ 4 | { 5 | name: 'main', 6 | entry: './page-express-mapper.js', 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: 'page-express-mapper.js', 10 | library: 'page-express-mapper', 11 | libraryTarget: 'umd', 12 | globalObject: 'this' 13 | }, 14 | externals: { 15 | fs: 'commonjs fs', 16 | path: 'commonjs path' 17 | }, 18 | node: { 19 | __dirname: false 20 | } 21 | }, 22 | { 23 | name: 'sampleApp', 24 | entry: './sampleApp/sampleApp.js', 25 | output: { 26 | path: path.join(__dirname, '/sampleApp/build'), 27 | filename: 'sampleApp.js', 28 | library: 'sampleApp', 29 | libraryTarget: 'umd', 30 | globalObject: 'this' 31 | }, 32 | externals: { 33 | fs: 'commonjs fs', 34 | path: 'commonjs path' 35 | }, 36 | node: { 37 | __dirname: false 38 | } 39 | } 40 | ] 41 | --------------------------------------------------------------------------------