From acb83192f3b516ab01de390b58ffba982304232d Mon Sep 17 00:00:00 2001 From: Hoang Date: Mon, 20 Sep 2021 14:21:32 +0700 Subject: [PATCH] Remove event emitter from store (#347) * Remove event emitter from store * Simplify implementation * Fix style changes --- .prettierrc | 3 + README.md | 57 ++-- jest.config.js | 7 +- package.json | 4 +- src/compat.ts | 25 +- src/connect.ts | 15 - src/core.ts | 5 +- src/store/memory.ts | 56 ++-- src/types.ts | 31 +- src/withSession.ts | 2 +- test/integration/next-test-utils.ts | 441 ++++++++++++++-------------- test/unit/index.test.ts | 45 +-- yarn.lock | 33 --- 13 files changed, 308 insertions(+), 416 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..544138b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/README.md b/README.md index 99f96cb..ce818d9 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ [![codecov](https://codecov.io/gh/hoangvvo/next-session/branch/master/graph/badge.svg)](https://codecov.io/gh/hoangvvo/next-session) [![PRs Welcome](https://badgen.net/badge/PRs/welcome/ff5252)](CONTRIBUTING.md) -Simple *promise-based* session middleware for [Next.js](https://github.com/zeit/next.js). Also works in [micro](https://github.com/zeit/micro) or [Node.js HTTP Server](https://nodejs.org/api/http.html), [Express](https://github.com/expressjs/express), and more. - +Simple _promise-based_ session middleware for [Next.js](https://github.com/zeit/next.js). Also works in [micro](https://github.com/zeit/micro) or [Node.js HTTP Server](https://nodejs.org/api/http.html), [Express](https://github.com/expressjs/express), and more. > Also check out alternatives like [express-session](https://github.com/expressjs/session)+[next-connect](https://github.com/hoangvvo/next-connect) or [next-iron-session](https://github.com/vvo/next-iron-session) instead. > Update: It is observed that express-session sometimes does not work properly with Next.js 11.x @@ -46,20 +45,18 @@ export const config = { api: { externalResolver: true, }, -} +}; ``` ...or setting `options.autoCommit` to `false` and do `await session.commit()` (See [this](https://github.com/hoangvvo/next-session#reqsessioncommit)). #### `{ session }` -**Note:** If you intend to call `session()` in multiple places, consider doing it only once and exporting it for elsewhere to avoid exceeded listeners. - ```javascript import { session } from 'next-session'; import nextConnect from 'next-connect'; -const mySession = session({ ...options }); +const mySession = session(options); const handler = nextConnect() .use(mySession) @@ -68,7 +65,7 @@ const handler = nextConnect() res.send( `In this session, you have visited this website ${req.session.views} time(s).` ); - }) + }); export default handler; ``` @@ -140,7 +137,7 @@ export default withSession(Page, options); ```javascript import { applySession } from 'next-session'; -export default function Page({views}) { +export default function Page({ views }) { return (
In this session, you have visited this website {views} time(s).
); @@ -151,9 +148,9 @@ export async function getServerSideProps({ req, res }) { req.session.views = req.session.views ? req.session.views + 1 : 1; return { props: { - views: req.session.views - } - } + views: req.session.views, + }, + }; } ``` @@ -179,21 +176,21 @@ await applySession(req, res, options); `next-session` accepts the properties below. -| options | description | default | -|---------|-------------|---------| -| name | The name of the cookie to be read from the request and set to the response. | `sid` | -| store | The session store instance to be used. | `MemoryStore` | -| genid | The function that generates a string for a new session ID. | [`nanoid`](https://github.com/ai/nanoid) | -| encode | Transforms session ID before setting cookie. It takes the raw session ID and returns the decoded/decrypted session ID. | undefined | -| decode | Transforms session ID back while getting from cookie. It should return the encoded/encrypted session ID | undefined | -| touchAfter | Only touch after an amount of time. Disabled by default or if set to `-1`. See [touchAfter](#touchAfter). | `-1` (Disabled) | -| autoCommit | Automatically commit session. Disable this if you want to manually `session.commit()` | `true` | -| cookie.secure | Specifies the boolean value for the **Secure** `Set-Cookie` attribute. | `false` | -| cookie.httpOnly | Specifies the boolean value for the **httpOnly** `Set-Cookie` attribute. | `true` | -| cookie.path | Specifies the value for the **Path** `Set-Cookie` attribute. | `/` | -| cookie.domain | Specifies the value for the **Domain** `Set-Cookie` attribute. | unset | -| cookie.sameSite | Specifies the value for the **SameSite** `Set-Cookie` attribute. | unset | -| cookie.maxAge | **(in seconds)** Specifies the value for the **Max-Age** `Set-Cookie` attribute. | unset (Browser session) | +| options | description | default | +| --------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| name | The name of the cookie to be read from the request and set to the response. | `sid` | +| store | The session store instance to be used. | `MemoryStore` | +| genid | The function that generates a string for a new session ID. | [`nanoid`](https://github.com/ai/nanoid) | +| encode | Transforms session ID before setting cookie. It takes the raw session ID and returns the decoded/decrypted session ID. | undefined | +| decode | Transforms session ID back while getting from cookie. It should return the encoded/encrypted session ID | undefined | +| touchAfter | Only touch after an amount of time. Disabled by default or if set to `-1`. See [touchAfter](#touchAfter). | `-1` (Disabled) | +| autoCommit | Automatically commit session. Disable this if you want to manually `session.commit()` | `true` | +| cookie.secure | Specifies the boolean value for the **Secure** `Set-Cookie` attribute. | `false` | +| cookie.httpOnly | Specifies the boolean value for the **httpOnly** `Set-Cookie` attribute. | `true` | +| cookie.path | Specifies the value for the **Path** `Set-Cookie` attribute. | `/` | +| cookie.domain | Specifies the value for the **Domain** `Set-Cookie` attribute. | unset | +| cookie.sameSite | Specifies the value for the **SameSite** `Set-Cookie` attribute. | unset | +| cookie.maxAge | **(in seconds)** Specifies the value for the **Max-Age** `Set-Cookie` attribute. | unset (Browser session) | ### touchAfter @@ -203,7 +200,7 @@ In `autoCommit` mode (which is enabled by default), for optimization, a session ### encode/decode -You may supply a custom pair of function that *encode/decode* or *encrypt/decrypt* the cookie on every request. +You may supply a custom pair of function that _encode/decode_ or _encrypt/decrypt_ the cookie on every request. ```javascript // `express-session` signing strategy @@ -238,7 +235,7 @@ if (loggedOut) await req.session.destroy(); ### req.session.commit() -Save the session and set neccessary headers. Return Promise. It must be called before *sending the headers (`res.writeHead`) or response (`res.send`, `res.end`, etc.)*. +Save the session and set neccessary headers. Return Promise. It must be called before _sending the headers (`res.writeHead`) or response (`res.send`, `res.end`, etc.)_. You **must** call this if `autoCommit` is set to `false`. @@ -254,7 +251,7 @@ The unique id that associates to the current session. ### req.session.isNew -Return *true* if the session is new. +Return _true_ if the session is new. ## Session Store @@ -282,7 +279,7 @@ A compatible session store must include three functions: `set(sid, session)`, `g // Both of the below work! function get(sid) { - return promiseGetFn(sid) + return promiseGetFn(sid); } function get(sid, done) { diff --git a/jest.config.js b/jest.config.js index 1236d52..c0235cc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,9 +5,10 @@ module.exports = { collectCoverageFrom: ['src/**/*'], testMatch: ['**/*.test.ts'], bail: true, + verbose: false, globals: { 'ts-jest': { - diagnostics: false - } - } + diagnostics: false, + }, + }, }; diff --git a/package.json b/package.json index de2ecd0..56e6c99 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prepublish": "yarn build", "build": "tsc --outDir dist", "lint": "eslint src --ext ts --ignore-path .gitignore", - "test": "yarn build && jest --colors --coverageReporters=text-lcov > coverage.lcov" + "test": "yarn build && jest --coverageReporters=text-lcov > coverage.lcov" }, "repository": { "type": "git", @@ -43,8 +43,6 @@ "@typescript-eslint/parser": "^4.31.1", "cookie-signature": "^1.1.0", "eslint": "^7.32.0", - "fs-extra": "^10.0.0", - "get-port": "^5.1.1", "jest": "^27.2.0", "next": "11.1.2", "react": "^17.0.2", diff --git a/src/compat.ts b/src/compat.ts index 21d6a5f..d7d2c78 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -1,34 +1,33 @@ import { EventEmitter } from 'events'; -import { Store as ExpressStore } from 'express-session'; import { callbackify, inherits } from 'util'; import MemoryStore from './store/memory'; -function CompatibleStore() { - // @ts-ignore - EventEmitter.call(this); -} -inherits(CompatibleStore, EventEmitter); - // no-op for compat function expressSession(options?: any): any {} -expressSession.Store = CompatibleStore; - -function CallbackMemoryStore() {} -inherits(CallbackMemoryStore, CompatibleStore); +function ExpressStore() { + // @ts-ignore + EventEmitter.call(this); +} +inherits(ExpressStore, EventEmitter); +expressSession.Store = ExpressStore; +function CallbackMemoryStore() { + // @ts-ignore + this.store = new Map(); +} +inherits(CallbackMemoryStore, ExpressStore); CallbackMemoryStore.prototype.get = callbackify(MemoryStore.prototype.get); CallbackMemoryStore.prototype.set = callbackify(MemoryStore.prototype.set); CallbackMemoryStore.prototype.destroy = callbackify( MemoryStore.prototype.destroy ); -CallbackMemoryStore.prototype.all = callbackify(MemoryStore.prototype.all); expressSession.MemoryStore = CallbackMemoryStore; export { expressSession }; -export function promisifyStore(store: ExpressStore): ExpressStore { +export function promisifyStore(store: any) { console.warn( 'promisifyStore has been deprecated: express-session store still works without using this.' ); diff --git a/src/connect.ts b/src/connect.ts index cf31ffd..33840f5 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -2,27 +2,12 @@ import { IncomingMessage, ServerResponse } from 'http'; import { applySession } from './core'; import { Options } from './types'; -let storeReady = true; - export default function session(opts?: Options) { - // store readiness - if (opts && opts.store && opts.store.on) { - opts.store.on('disconnect', () => { - storeReady = false; - }); - opts.store.on('connect', () => { - storeReady = true; - }); - } return ( req: IncomingMessage, res: ServerResponse, next: (err?: any) => void ) => { - if (!storeReady) { - next(); - return; - } applySession(req, res, opts).then(next); }; } diff --git a/src/core.ts b/src/core.ts index ce59351..84cc39f 100644 --- a/src/core.ts +++ b/src/core.ts @@ -134,8 +134,9 @@ export async function applySession( req.session.isNew = false; req.session.id = sessId!; // Some store return cookie.expires as string, convert it to Date - if (typeof req.session.cookie.expires === 'string') + if (typeof req.session.cookie.expires === 'string') { req.session.cookie.expires = new Date(req.session.cookie.expires); + } } else { req.session = { cookie: { @@ -146,7 +147,7 @@ export async function applySession( secure: options.cookie?.secure || false, ...(options.cookie?.maxAge ? { maxAge: options.cookie.maxAge, expires: new Date() } - : { maxAge: null }), + : { maxAge: undefined }), }, commit, destroy, diff --git a/src/store/memory.ts b/src/store/memory.ts index 5aa7777..aec22e9 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -1,52 +1,36 @@ -import { EventEmitter } from 'events'; import { SessionData, SessionStore } from '../types'; -export default class MemoryStore extends EventEmitter implements SessionStore { - public sessions: Record = {}; +export default class MemoryStore implements SessionStore { + private store = new Map(); - constructor() { - super(); - } - - get(sid: string): Promise { - const self = this; - - const sess = this.sessions[sid]; + async get(sid: string): Promise { + const sess = this.store.get(sid); if (sess) { - const session = JSON.parse(sess); - session.cookie.expires = session.cookie.expires - ? new Date(session.cookie.expires) - : null; - + const session = JSON.parse(sess, (key, value) => { + if (key === 'expires') return new Date(value); + return value; + }) as SessionData; if ( - !session.cookie.expires || - Date.now() < session.cookie.expires.getTime() + session.cookie.expires && + session.cookie.expires.getTime() <= Date.now() ) { - // check expires before returning - return Promise.resolve(session); + await this.destroy(sid); + return null; } - - self.destroy(sid); - return Promise.resolve(null); + return session; } - return Promise.resolve(null); - } - - set(sid: string, sess: SessionData) { - this.sessions[sid] = JSON.stringify(sess); - return Promise.resolve(); + return null; } - touch(sid: string, session: SessionData) { - return this.set(sid, session); + async set(sid: string, sess: SessionData) { + this.store.set(sid, JSON.stringify(sess)); } - all() { - return Promise.resolve(Object.values(this.sessions)); + async destroy(sid: string) { + this.store.delete(sid); } - destroy(sid: string) { - delete this.sessions[sid]; - return Promise.resolve(); + async touch(sid: string, sess: SessionData) { + this.store.set(sid, JSON.stringify(sess)); } } diff --git a/src/types.ts b/src/types.ts index ebf5477..544320e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Store as ExpressStore } from 'express-session'; +import { Store as IExpressStore } from 'express-session'; export type SessionData = { [key: string]: any; @@ -13,36 +13,39 @@ export interface Session extends SessionData { } export type SessionCookieData = { - path: string; - secure: boolean; httpOnly: boolean; + path: string; domain?: string | undefined; + secure: boolean; sameSite?: boolean | 'lax' | 'strict' | 'none'; -} & ({ maxAge: number; expires: Date } | { maxAge: null; expires?: undefined }); +} & ( + | { maxAge: undefined; expires?: undefined } + | { + maxAge: number; + expires: Date; + } +); export abstract class SessionStore { abstract get(sid: string): Promise; abstract set(sid: string, sess: SessionData): Promise; abstract destroy(sid: string): Promise; abstract touch?(sid: string, sess: SessionData): Promise; - on?(event: string | symbol, listener: (...args: any[]) => void): this; } export interface Options { name?: string; - store?: SessionStore | ExpressStore; + store?: SessionStore | IExpressStore; genid?: () => string; encode?: (rawSid: string) => string; decode?: (encryptedSid: string) => string | null; touchAfter?: number; - cookie?: { - secure?: boolean; - httpOnly?: boolean; - path?: string; - domain?: string; - sameSite?: boolean | 'lax' | 'strict' | 'none'; - maxAge?: number | null; - }; + cookie?: Partial< + Pick< + SessionCookieData, + 'maxAge' | 'httpOnly' | 'path' | 'domain' | 'secure' | 'sameSite' + > + >; autoCommit?: boolean; /** * @deprecated diff --git a/src/withSession.ts b/src/withSession.ts index e7c70b6..560c484 100644 --- a/src/withSession.ts +++ b/src/withSession.ts @@ -2,7 +2,7 @@ import { NextApiHandler, NextComponentType, NextPage, - NextPageContext + NextPageContext, } from 'next'; import { createElement } from 'react'; import { applySession } from './core'; diff --git a/test/integration/next-test-utils.ts b/test/integration/next-test-utils.ts index e85f54f..85cc174 100644 --- a/test/integration/next-test-utils.ts +++ b/test/integration/next-test-utils.ts @@ -1,32 +1,27 @@ // @ts-nocheck -import spawn from 'cross-spawn' +import spawn from 'cross-spawn'; // import express from 'express' -import { - existsSync, - readFileSync, - unlinkSync, - writeFileSync -} from 'fs' -import { writeFile } from 'fs-extra' -import getPort from 'get-port' -import http from 'http' +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +// import { writeFile } from 'fs-extra' +// import getPort from 'get-port' +import http from 'http'; // `next` here is the symlink in `test/node_modules/next` which points to the root directory. // This is done so that requiring from `next` works. // The reason we don't import the relative path `../../dist/` is that it would lead to inconsistent module singletons -import server from 'next/dist/server/next' -import _pkg from 'next/package.json' -import fetch from 'node-fetch' -import path from 'path' -import qs from 'querystring' -import treeKill from 'tree-kill' +import server from 'next/dist/server/next'; +import _pkg from 'next/package.json'; +import fetch from 'node-fetch'; +import path from 'path'; +import qs from 'querystring'; +import treeKill from 'tree-kill'; -export const nextServer = server -export const pkg = _pkg +export const nextServer = server; +export const pkg = _pkg; // polyfill Object.fromEntries for the test/integration/relay-analytics tests // on node 10, this can be removed after we no longer support node 10 if (!Object.fromEntries) { - Object.fromEntries = require('core-js/features/object/from-entries') + Object.fromEntries = require('core-js/features/object/from-entries'); } export function initNextServerScript( @@ -40,98 +35,98 @@ export function initNextServerScript( const instance = spawn('node', ['--no-deprecation', scriptPath], { env, cwd: opts && opts.cwd, - }) + }); function handleStdout(data) { - const message = data.toString() + const message = data.toString(); if (successRegexp.test(message)) { - resolve(instance) + resolve(instance); } - process.stdout.write(message) + process.stdout.write(message); if (opts && opts.onStdout) { - opts.onStdout(message.toString()) + opts.onStdout(message.toString()); } } function handleStderr(data) { - const message = data.toString() + const message = data.toString(); if (failRegexp && failRegexp.test(message)) { - instance.kill() - return reject(new Error('received failRegexp')) + instance.kill(); + return reject(new Error('received failRegexp')); } - process.stderr.write(message) + process.stderr.write(message); if (opts && opts.onStderr) { - opts.onStderr(message.toString()) + opts.onStderr(message.toString()); } } - instance.stdout.on('data', handleStdout) - instance.stderr.on('data', handleStderr) + instance.stdout.on('data', handleStdout); + instance.stderr.on('data', handleStderr); instance.on('close', () => { - instance.stdout.removeListener('data', handleStdout) - instance.stderr.removeListener('data', handleStderr) - }) + instance.stdout.removeListener('data', handleStdout); + instance.stderr.removeListener('data', handleStderr); + }); instance.on('error', (err) => { - reject(err) - }) - }) + reject(err); + }); + }); } export function getFullUrl(appPortOrUrl, url, hostname) { let fullUrl = typeof appPortOrUrl === 'string' && appPortOrUrl.startsWith('http') ? appPortOrUrl - : `http://${hostname ? hostname : 'localhost'}:${appPortOrUrl}${url}` + : `http://${hostname ? hostname : 'localhost'}:${appPortOrUrl}${url}`; if (typeof appPortOrUrl === 'string' && url) { - const parsedUrl = new URL(fullUrl) - const parsedPathQuery = new URL(url, fullUrl) + const parsedUrl = new URL(fullUrl); + const parsedPathQuery = new URL(url, fullUrl); - parsedUrl.search = parsedPathQuery.search - parsedUrl.pathname = parsedPathQuery.pathname - fullUrl = parsedUrl.toString() + parsedUrl.search = parsedPathQuery.search; + parsedUrl.pathname = parsedPathQuery.pathname; + fullUrl = parsedUrl.toString(); } - return fullUrl + return fullUrl; } export function renderViaAPI(app, pathname, query) { - const url = `${pathname}${query ? `?${qs.stringify(query)}` : ''}` - return app.renderToHTML({ url }, {}, pathname, query) + const url = `${pathname}${query ? `?${qs.stringify(query)}` : ''}`; + return app.renderToHTML({ url }, {}, pathname, query); } export function renderViaHTTP(appPort, pathname, query, opts) { - return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text()) + return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text()); } export function fetchViaHTTP(appPort, pathname, query, opts) { const url = `${pathname}${ typeof query === 'string' ? query : query ? `?${qs.stringify(query)}` : '' - }` - return fetch(getFullUrl(appPort, url), opts) + }`; + return fetch(getFullUrl(appPort, url), opts); } -export function findPort() { - return getPort() -} +// export function findPort() { +// return getPort() +// } export function runNextCommand(argv, options = {}) { - const nextDir = path.dirname(require.resolve('next/package')) - const nextBin = path.join(nextDir, 'dist/bin/next') - const cwd = options.cwd || nextDir + const nextDir = path.dirname(require.resolve('next/package')); + const nextBin = path.join(nextDir, 'dist/bin/next'); + const cwd = options.cwd || nextDir; // Let Next.js decide the environment const env = { ...process.env, ...options.env, NODE_ENV: '', __NEXT_TEST_MODE: 'true', - } + }; return new Promise((resolve, reject) => { - console.log(`Running command "next ${argv.join(' ')}"`) + console.log(`Running command "next ${argv.join(' ')}"`); const instance = spawn( 'node', [...(options.nodeArgs || []), '--no-deprecation', nextBin, ...argv], @@ -141,32 +136,32 @@ export function runNextCommand(argv, options = {}) { env, stdio: ['ignore', 'pipe', 'pipe'], } - ) + ); if (typeof options.instance === 'function') { - options.instance(instance) + options.instance(instance); } - let stderrOutput = '' + let stderrOutput = ''; if (options.stderr) { instance.stderr.on('data', function (chunk) { - stderrOutput += chunk + stderrOutput += chunk; if (options.stderr === 'log') { - console.log(chunk.toString()) + console.log(chunk.toString()); } - }) + }); } - let stdoutOutput = '' + let stdoutOutput = ''; if (options.stdout) { instance.stdout.on('data', function (chunk) { - stdoutOutput += chunk + stdoutOutput += chunk; if (options.stdout === 'log') { - console.log(chunk.toString()) + console.log(chunk.toString()); } - }) + }); } instance.on('close', (code, signal) => { @@ -176,7 +171,7 @@ export function runNextCommand(argv, options = {}) { !options.ignoreFail && code !== 0 ) { - return reject(new Error(`command failed with code ${code}`)) + return reject(new Error(`command failed with code ${code}`)); } resolve({ @@ -184,29 +179,29 @@ export function runNextCommand(argv, options = {}) { signal, stdout: stdoutOutput, stderr: stderrOutput, - }) - }) + }); + }); instance.on('error', (err) => { - err.stdout = stdoutOutput - err.stderr = stderrOutput - reject(err) - }) - }) + err.stdout = stdoutOutput; + err.stderr = stderrOutput; + reject(err); + }); + }); } export function runNextCommandDev(argv, stdOut, opts = {}) { - const nextDir = path.dirname(require.resolve('next/package')) - const nextBin = path.join(nextDir, 'dist/bin/next') - const cwd = opts.cwd || nextDir + const nextDir = path.dirname(require.resolve('next/package')); + const nextBin = path.join(nextDir, 'dist/bin/next'); + const cwd = opts.cwd || nextDir; const env = { ...process.env, NODE_ENV: undefined, __NEXT_TEST_MODE: 'true', ...opts.env, - } + }; - const nodeArgs = opts.nodeArgs || [] + const nodeArgs = opts.nodeArgs || []; return new Promise((resolve, reject) => { const instance = spawn( 'node', @@ -215,117 +210,117 @@ export function runNextCommandDev(argv, stdOut, opts = {}) { cwd, env, } - ) - let didResolve = false + ); + let didResolve = false; function handleStdout(data) { - const message = data.toString() + const message = data.toString(); const bootupMarkers = { dev: /compiled successfully/i, start: /started server/i, - } + }; if ( (opts.bootupMarker && opts.bootupMarker.test(message)) || bootupMarkers[opts.nextStart || stdOut ? 'start' : 'dev'].test(message) ) { if (!didResolve) { - didResolve = true - resolve(stdOut ? message : instance) + didResolve = true; + resolve(stdOut ? message : instance); } } if (typeof opts.onStdout === 'function') { - opts.onStdout(message) + opts.onStdout(message); } if (opts.stdout !== false) { - process.stdout.write(message) + process.stdout.write(message); } } function handleStderr(data) { - const message = data.toString() + const message = data.toString(); if (typeof opts.onStderr === 'function') { - opts.onStderr(message) + opts.onStderr(message); } if (opts.stderr !== false) { - process.stderr.write(message) + process.stderr.write(message); } } - instance.stdout.on('data', handleStdout) - instance.stderr.on('data', handleStderr) + instance.stdout.on('data', handleStdout); + instance.stderr.on('data', handleStderr); instance.on('close', () => { - instance.stdout.removeListener('data', handleStdout) - instance.stderr.removeListener('data', handleStderr) + instance.stdout.removeListener('data', handleStdout); + instance.stderr.removeListener('data', handleStderr); if (!didResolve) { - didResolve = true - resolve() + didResolve = true; + resolve(); } - }) + }); instance.on('error', (err) => { - reject(err) - }) - }) + reject(err); + }); + }); } // Launch the app in dev mode. export function launchApp(dir, port, opts) { - return runNextCommandDev([dir, '-p', port], undefined, opts) + return runNextCommandDev([dir, '-p', port], undefined, opts); } export function nextBuild(dir, args = [], opts = {}) { - return runNextCommand(['build', dir, ...args], opts) + return runNextCommand(['build', dir, ...args], opts); } export function nextExport(dir, { outdir }, opts = {}) { - return runNextCommand(['export', dir, '--outdir', outdir], opts) + return runNextCommand(['export', dir, '--outdir', outdir], opts); } export function nextExportDefault(dir, opts = {}) { - return runNextCommand(['export', dir], opts) + return runNextCommand(['export', dir], opts); } export function nextLint(dir, args = [], opts = {}) { - return runNextCommand(['lint', dir, ...args], opts) + return runNextCommand(['lint', dir, ...args], opts); } export function nextStart(dir, port, opts = {}) { return runNextCommandDev(['start', '-p', port, dir], undefined, { ...opts, nextStart: true, - }) + }); } export function buildTS(args = [], cwd, env = {}) { - cwd = cwd || path.dirname(require.resolve('next/package')) - env = { ...process.env, NODE_ENV: undefined, ...env } + cwd = cwd || path.dirname(require.resolve('next/package')); + env = { ...process.env, NODE_ENV: undefined, ...env }; return new Promise((resolve, reject) => { const instance = spawn( 'node', ['--no-deprecation', require.resolve('typescript/lib/tsc'), ...args], { cwd, env } - ) - let output = '' + ); + let output = ''; const handleData = (chunk) => { - output += chunk.toString() - } + output += chunk.toString(); + }; - instance.stdout.on('data', handleData) - instance.stderr.on('data', handleData) + instance.stdout.on('data', handleData); + instance.stderr.on('data', handleData); instance.on('exit', (code) => { if (code) { - return reject(new Error('exited with code: ' + code + '\n' + output)) + return reject(new Error('exited with code: ' + code + '\n' + output)); } - resolve() - }) - }) + resolve(); + }); + }); } // Kill a launched app @@ -344,31 +339,31 @@ export async function killApp(instance) { // Command failed: taskkill /pid 6924 /T /F // ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated. // Reason: There is no running instance of the task. - return resolve() + return resolve(); } - return reject(err) + return reject(err); } - resolve() - }) - }) + resolve(); + }); + }); } export async function startApp(app) { - await app.prepare() - const handler = app.getRequestHandler() - const server = http.createServer(handler) - server.__app = app + await app.prepare(); + const handler = app.getRequestHandler(); + const server = http.createServer(handler); + server.__app = app; - await promiseCall(server, 'listen') - return server + await promiseCall(server, 'listen'); + return server; } export async function stopApp(server) { if (server.__app) { - await server.__app.close() + await server.__app.close(); } - await promiseCall(server, 'close') + await promiseCall(server, 'close'); } export function promiseCall(obj, method, ...args) { @@ -376,17 +371,17 @@ export function promiseCall(obj, method, ...args) { const newArgs = [ ...args, function (err, res) { - if (err) return reject(err) - resolve(res) + if (err) return reject(err); + resolve(res); }, - ] + ]; - obj[method](...newArgs) - }) + obj[method](...newArgs); + }); } export function waitFor(millis) { - return new Promise((resolve) => setTimeout(resolve, millis)) + return new Promise((resolve) => setTimeout(resolve, millis)); } // export async function startStaticServer(dir, notFoundFile) { @@ -416,87 +411,87 @@ export function waitFor(millis) { // check for content in 1 second intervals timing out after // 30 seconds export async function check(contentFn, regex, hardError = true) { - let content - let lastErr + let content; + let lastErr; for (let tries = 0; tries < 30; tries++) { try { - content = await contentFn() + content = await contentFn(); if (typeof regex === 'string') { if (regex === content) { - return true + return true; } } else if (regex.test(content)) { // found the content - return true + return true; } - await waitFor(1000) + await waitFor(1000); } catch (err) { - await waitFor(1000) - lastErr = err + await waitFor(1000); + lastErr = err; } } - console.error('TIMED OUT CHECK: ', { regex, content, lastErr }) + console.error('TIMED OUT CHECK: ', { regex, content, lastErr }); if (hardError) { - throw new Error('TIMED OUT: ' + regex + '\n\n' + content) + throw new Error('TIMED OUT: ' + regex + '\n\n' + content); } - return false + return false; } export class File { constructor(path) { - this.path = path + this.path = path; this.originalContent = existsSync(this.path) ? readFileSync(this.path, 'utf8') - : null + : null; } write(content) { if (!this.originalContent) { - this.originalContent = content + this.originalContent = content; } - writeFileSync(this.path, content, 'utf8') + writeFileSync(this.path, content, 'utf8'); } replace(pattern, newValue) { - const currentContent = readFileSync(this.path, 'utf8') + const currentContent = readFileSync(this.path, 'utf8'); if (pattern instanceof RegExp) { if (!pattern.test(currentContent)) { throw new Error( `Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}` - ) + ); } } else if (typeof pattern === 'string') { if (!currentContent.includes(pattern)) { throw new Error( `Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}` - ) + ); } } else { - throw new Error(`Unknown replacement attempt type: ${pattern}`) + throw new Error(`Unknown replacement attempt type: ${pattern}`); } - const newContent = currentContent.replace(pattern, newValue) - this.write(newContent) + const newContent = currentContent.replace(pattern, newValue); + this.write(newContent); } delete() { - unlinkSync(this.path) + unlinkSync(this.path); } restore() { - this.write(this.originalContent) + this.write(this.originalContent); } } export async function evaluate(browser, input) { if (typeof input === 'function') { - const result = await browser.eval(input) - await new Promise((resolve) => setTimeout(resolve, 30)) - return result + const result = await browser.eval(input); + await new Promise((resolve) => setTimeout(resolve, 30)); + return result; } else { - throw new Error(`You must pass a function to be evaluated in the browser.`) + throw new Error(`You must pass a function to be evaluated in the browser.`); } } @@ -504,31 +499,31 @@ export async function retry(fn, duration = 3000, interval = 500, description) { if (duration % interval !== 0) { throw new Error( `invalid duration ${duration} and interval ${interval} mix, duration must be evenly divisible by interval` - ) + ); } for (let i = duration; i >= 0; i -= interval) { try { - return await fn() + return await fn(); } catch (err) { if (i === 0) { console.error( `Failed to retry${ description ? ` ${description}` : '' } within ${duration}ms` - ) - throw err + ); + throw err; } console.warn( `Retrying${description ? ` ${description}` : ''} in ${interval}ms` - ) - await waitFor(interval) + ); + await waitFor(interval); } } } export async function hasRedbox(browser, expected = true) { - let attempts = 30 + let attempts = 30; do { const has = await evaluate(browser, () => { return Boolean( @@ -539,18 +534,18 @@ export async function hasRedbox(browser, expected = true) { '#nextjs__container_errors_label, #nextjs__container_build_error_label' ) ) - ) - }) + ); + }); if (has) { - return true + return true; } if (--attempts < 0) { - break + break; } - await new Promise((resolve) => setTimeout(resolve, 1000)) - } while (expected) - return false + await new Promise((resolve) => setTimeout(resolve, 1000)); + } while (expected); + return false; } export async function getRedboxHeader(browser) { @@ -559,16 +554,18 @@ export async function getRedboxHeader(browser) { evaluate(browser, () => { const portal = [].slice .call(document.querySelectorAll('nextjs-portal')) - .find((p) => p.shadowRoot.querySelector('[data-nextjs-dialog-header')) - const root = portal.shadowRoot + .find((p) => + p.shadowRoot.querySelector('[data-nextjs-dialog-header') + ); + const root = portal.shadowRoot; return root .querySelector('[data-nextjs-dialog-header]') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown'); }), 3000, 500, 'getRedboxHeader' - ) + ); } export async function getRedboxSource(browser) { @@ -581,16 +578,16 @@ export async function getRedboxSource(browser) { p.shadowRoot.querySelector( '#nextjs__container_errors_label, #nextjs__container_build_error_label' ) - ) - const root = portal.shadowRoot + ); + const root = portal.shadowRoot; return root .querySelector('[data-nextjs-codeframe], [data-nextjs-terminal]') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown'); }), 3000, 500, 'getRedboxSource' - ) + ); } export async function getRedboxDescription(browser) { @@ -601,90 +598,90 @@ export async function getRedboxDescription(browser) { .call(document.querySelectorAll('nextjs-portal')) .find((p) => p.shadowRoot.querySelector('[data-nextjs-dialog-header]') - ) - const root = portal.shadowRoot + ); + const root = portal.shadowRoot; return root .querySelector('#nextjs__container_errors_desc') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown'); }), 3000, 500, 'getRedboxDescription' - ) + ); } export function getBrowserBodyText(browser) { - return browser.eval('document.getElementsByTagName("body")[0].innerText') + return browser.eval('document.getElementsByTagName("body")[0].innerText'); } export function normalizeRegEx(src) { - return new RegExp(src).source.replace(/\^\//g, '^\\/') + return new RegExp(src).source.replace(/\^\//g, '^\\/'); } function readJson(path) { - return JSON.parse(readFileSync(path)) + return JSON.parse(readFileSync(path)); } export function getBuildManifest(dir) { - return readJson(path.join(dir, '.next/build-manifest.json')) + return readJson(path.join(dir, '.next/build-manifest.json')); } export function getPageFileFromBuildManifest(dir, page) { - const buildManifest = getBuildManifest(dir) - const pageFiles = buildManifest.pages[page] + const buildManifest = getBuildManifest(dir); + const pageFiles = buildManifest.pages[page]; if (!pageFiles) { - throw new Error(`No files for page ${page}`) + throw new Error(`No files for page ${page}`); } const pageFile = pageFiles.find( (file) => file.endsWith('.js') && file.includes(`pages${page === '' ? '/index' : page}`) - ) + ); if (!pageFile) { - throw new Error(`No page file for page ${page}`) + throw new Error(`No page file for page ${page}`); } - return pageFile + return pageFile; } export function readNextBuildClientPageFile(appDir, page) { - const pageFile = getPageFileFromBuildManifest(appDir, page) - return readFileSync(path.join(appDir, '.next', pageFile), 'utf8') + const pageFile = getPageFileFromBuildManifest(appDir, page); + return readFileSync(path.join(appDir, '.next', pageFile), 'utf8'); } export function getPagesManifest(dir) { - const serverFile = path.join(dir, '.next/server/pages-manifest.json') + const serverFile = path.join(dir, '.next/server/pages-manifest.json'); if (existsSync(serverFile)) { - return readJson(serverFile) + return readJson(serverFile); } - return readJson(path.join(dir, '.next/serverless/pages-manifest.json')) + return readJson(path.join(dir, '.next/serverless/pages-manifest.json')); } -export function updatePagesManifest(dir, content) { - const serverFile = path.join(dir, '.next/server/pages-manifest.json') +// export function updatePagesManifest(dir, content) { +// const serverFile = path.join(dir, '.next/server/pages-manifest.json') - if (existsSync(serverFile)) { - return writeFile(serverFile, content) - } - return writeFile( - path.join(dir, '.next/serverless/pages-manifest.json'), - content - ) -} +// if (existsSync(serverFile)) { +// return writeFile(serverFile, content) +// } +// return writeFile( +// path.join(dir, '.next/serverless/pages-manifest.json'), +// content +// ) +// } export function getPageFileFromPagesManifest(dir, page) { - const pagesManifest = getPagesManifest(dir) - const pageFile = pagesManifest[page] + const pagesManifest = getPagesManifest(dir); + const pageFile = pagesManifest[page]; if (!pageFile) { - throw new Error(`No file for page ${page}`) + throw new Error(`No file for page ${page}`); } - return pageFile + return pageFile; } export function readNextBuildServerPageFile(appDir, page) { - const pageFile = getPageFileFromPagesManifest(appDir, page) - return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8') + const pageFile = getPageFileFromPagesManifest(appDir, page); + return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8'); } diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index e0e94b4..1e4c5f9 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -5,14 +5,13 @@ import { createServer, IncomingMessage, RequestListener } from 'http'; import { NextApiHandler, NextComponentType, NextPage } from 'next'; import React from 'react'; import request from 'supertest'; -import { parse } from 'url'; import { applySession, expressSession, promisifyStore, session, SessionData, - withSession + withSession, } from '../../src'; import MemoryStore from '../../src/store/memory'; import { Options } from '../../src/types'; @@ -109,7 +108,6 @@ describe('applySession', () => { .expect('1') .then(({ header }) => expect(header).not.toHaveProperty('set-cookie')); await agent.delete('/'); - expect(Object.keys(store.sessions).length).toBe(0); await agent .get('/') .expect('0') @@ -420,24 +418,6 @@ describe('connect middleware', () => { }); expect(request.session).toBeTruthy(); }); - - test('respects storeReady', async () => { - const store = new MemoryStore(); - const server = setUpServer(defaultHandler, false, async (req, res) => { - await new Promise((resolve) => { - session({ store })(req, res, resolve); - }); - }); - await request(server).get('/'); - store.emit('disconnect'); - await request(server) - .get('/') - .then(({ header }) => expect(header).not.toHaveProperty('set-cookie')); - store.emit('connect'); - await request(server) - .get('/') - .then(({ header }) => expect(header).toHaveProperty('set-cookie')); - }); }); describe('Store', () => { @@ -521,29 +501,6 @@ describe('promisifyStore', () => { }); describe('MemoryStore', () => { - test('should show every session', async () => { - const store = new MemoryStore(); - store.sessions = {}; - const server = setUpServer( - async (req, res) => { - if (req.url === '/all') { - const ss = (await (req as any).sessionStore.all()).map( - (sess: string) => JSON.parse(sess).user - ); - res.end(ss.toString()); - } else { - req.session.user = parse(req.url as string, true).query.user; - res.end(); - } - }, - { store } - ); - await request(server).get('/').query('user=squidward'); - await request(server).get('/').query('user=spongebob'); - await request(server).get('/').query('user=patrick'); - await request(server).get('/all').expect('squidward,spongebob,patrick'); - }); - test('should expire session', async () => { const sessionStore = new MemoryStore(); let sessionId: string | undefined | null; diff --git a/yarn.lock b/yarn.lock index cfb10a0..5cd260d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2446,15 +2446,6 @@ formidable@^1.2.2: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== -fs-extra@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2511,11 +2502,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -2589,11 +2575,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -3507,15 +3488,6 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -4958,11 +4930,6 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"