Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ryota-ka committed Oct 10, 2018
0 parents commit a1d56b7
Show file tree
Hide file tree
Showing 15 changed files with 4,389 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
version: 2
jobs:
build:
docker:
- image: node
steps:
- checkout
- run:
name: Install dependencies
command: |
yarn
- run:
name: Run Prettier
command: |
yarn prettier:check
- run:
name: Build
command: |
yarn tsc --noEmit
- run:
name: Run tests
command: |
yarn test
workflows:
version: 2
build:
jobs:
- build
80 changes: 80 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
### https://raw.github.com/github/gitignore/72190ee30bd1e2ccc233222341435adacb7a6500/Node.gitignore

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless

/src/**/*.js

lib/*
!.keep
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.keep
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parser: typescript
printWidth: 120
singleQuote: true
tabWidth: 4
trailingComma: all
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# koa-path-canonicalizer

[![npm version](https://img.shields.io/npm/v/koa-path-canonicalizer.svg?style=flat-square)](https://www.npmjs.com/package/koa-path-canonicalizer)
[![CircleCI](https://circleci.com/gh/herp-inc/koa-path-canonicalizer.svg?style=shield)](https://circleci.com/gh/herp-inc/koa-path-canonicalizer)

A Koa middleware to redirect to canonicalized path

## Installation

```console
$ yarn add koa-path-canonicalizer
```

## Usage

```javascript
const Koa = require('koa');
const { pathCanonicalizer } = require('koa-path-canonicalizer');

const app = new Koa();

app.use(pathCanonicalizer());
```

## License

The MIT License (MIT)
6 changes: 6 additions & 0 deletions bin/prerelease.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh -eu

yarn install
rm -rf ./lib/*
yarn tsc -d --outDir ./lib
cp .npmignore package.json README.md ./lib
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'jsx', 'node', 'ts', 'tsx'],
testRegex: '(\\.|/)(test|spec)\\.tsx?$',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};
Empty file added lib/.keep
Empty file.
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "koa-path-canonicalizer",
"version": "0.1.0",
"description": "A Koa middleware to redirect to canonicalized path",
"main": "index.js",
"repository": "https://github.com/herp-inc/koa-path-canonicalizer.git",
"author": "Ryota Kameoka <ryota.kameoka@herp.co.jp>",
"license": "MIT",
"typings": "index.d.ts",
"devDependencies": {
"@types/jest": "^23.3.4",
"@types/koa": "^2.0.46",
"@types/node": "^10.11.5",
"@types/supertest": "^2.0.6",
"jest": "^23.6.0",
"koa": "^2.5.3",
"prettier": "^1.14.3",
"supertest": "^3.3.0",
"ts-jest": "^23.10.4",
"typescript": "^3.1.1"
},
"scripts": {
"prerelease": "./bin/prerelease.sh",
"prettier:check": "prettier --list-different src/**/*.ts",
"prettier:format": "prettier --write src/**/*.ts",
"test": "jest"
}
}
27 changes: 27 additions & 0 deletions src/canonicalize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { canonicalize } from './canonicalize';

describe(canonicalize, () => {
it('returns null when the given path is /', () => {
expect(canonicalize('/')).toBeNull();
});

it('returns / when the given path is //', () => {
expect(canonicalize('//')).toBe('/');
});

it('returns null when the given path is already canonical', () => {
expect(canonicalize('/api/v1/users/42')).toBeNull();
});

it('eliminates trailing slashes', () => {
expect(canonicalize('/api/v1/users/42/')).toBe('/api/v1/users/42');
expect(canonicalize('/api/v1/users/42//')).toBe('/api/v1/users/42');
expect(canonicalize('/api/v1/users/42///')).toBe('/api/v1/users/42');
});

it('folds multiple slashes into one', () => {
expect(canonicalize('//api/v1/users/42')).toBe('/api/v1/users/42');
expect(canonicalize('/api/v1//users//42')).toBe('/api/v1/users/42');
expect(canonicalize('/api/v1///users///42')).toBe('/api/v1/users/42');
});
});
23 changes: 23 additions & 0 deletions src/canonicalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const SLASHES = new RegExp('//+', 'g');

/**
* Canonicalize the given path. Returns null if the given path is already canonical.
* @param path
*/
export function canonicalize(path: string): string | null {
if (path === '/') {
return null;
}

let newPath = path;

if (newPath.includes('//')) {
newPath = newPath.replace(SLASHES, '/');
}

if (newPath !== '/' && newPath.endsWith('/')) {
newPath = newPath.slice(0, -1);
}

return newPath === path ? null : newPath;
}
62 changes: 62 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as Koa from 'koa';
import { Server } from 'http';
import * as request from 'supertest';

import { pathCanonicalizer } from './';

describe(pathCanonicalizer, () => {
let app: Koa;
let server: Server;

beforeAll(() => {
app = new Koa();
app.use(pathCanonicalizer());
app.use(async (ctx, next) => {
if (ctx.path === '/' || ctx.path === '/api/v1/users/42') {
ctx.status = 200;
ctx.body = 'Hello, world!';
await next();
}
});

server = app.listen();
});

afterAll(() => server.close());

const get = (path: string) => request(server).get(path);

describe('when the path is canonical', () => {
describe('/', () => {
it('returns 200', async () => {
const res = await get('/');
expect(res.status).toBe(200);
});
});

describe('/api/v1/users/42', () => {
it('returns 200', async () => {
const res = await get('/api/v1/users/42');
expect(res.status).toBe(200);
});
});
});

describe('when the path is non-canonical', () => {
describe('//', () => {
it('redirects to /', async () => {
const res = await get('//');
expect(res.status).toBe(302);
expect(res.header['location']).toBe('/');
});
});

describe('//api////v1/users////42', () => {
it('redirects to /api/v1/users/42', async () => {
const res = await get('//api////v1/users////42');
expect(res.status).toBe(302);
expect(res.header['location']).toBe('/api/v1/users/42');
});
});
});
});
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Middleware, Context } from 'koa';

import { canonicalize } from './canonicalize';

/**
* A Koa middleware to redirect to canonicalized path
*/
export function pathCanonicalizer(): Middleware {
return async function(ctx: Context, next: () => Promise<void>): Promise<void> {
const newPath = canonicalize(ctx.path);

if (newPath === null) {
await next();
} else {
ctx.redirect(newPath);
}
};
}
14 changes: 14 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"target": "esnext"
},
"exclude": [
"./src/**/*.test.ts"
]
}
Loading

0 comments on commit a1d56b7

Please sign in to comment.