├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nuxtrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── playground ├── app.vue ├── nuxt.config.ts └── package.json ├── src ├── module.ts └── runtime │ ├── cache.inmemory.ts │ ├── cache.middleware.ts │ ├── cache.nitro.ts │ ├── cache.utils.ts │ ├── generateFlags.ts │ └── types │ └── index.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | nuxt-cache-ssr -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs/eslint-config-typescript" 4 | ], 5 | "rules": { 6 | "@typescript-eslint/no-unused-vars": [ 7 | "off" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bhaskar gyan vardhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-cache-ssr 2 | [![NPM version](https://img.shields.io/npm/v/nuxt-cache-ssr.svg)](https://www.npmjs.com/package/nuxt-cache-ssr) 3 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 4 | 5 | 6 | In Memory Cache middleware for Nuxt3 SSR rendering . 7 | 8 | ## TODO 9 | 10 | - [x] In Memory cache options 11 | - [x] Custom Key for Page Cache 12 | - [x] option to disable per enviroment 13 | - [x] Compress cached response (experimental) 14 | - [x] Disble page cached on demand (experimental) 15 | - [ ] Regex for Pages 16 | - [ ] Redis Cache 17 | - [ ] Auto refresh cache before expiry (?) 18 | 19 | ## Setup 20 | ```npm install nuxt-cache-ssr``` 21 | 22 | or 23 | 24 | ```yarn add nuxt-cache-ssr``` 25 | 26 | then inside your `nuxt.config.js` add cache config: 27 | 28 | ```javascript 29 | export default defineNuxtConfig({ 30 | modules: [ 31 | ['nuxt-cache-ssr', { 32 | // Can be disable per enviroment, like in dev 33 | enabled: true, 34 | store: { 35 | // Plceholder for store type, will be usable after Redis Release 36 | type: 'memory', 37 | // maximum number of pages to store in memory 38 | // if limit is reached, least recently used page 39 | // is removed. 40 | max: 500, 41 | // number of Millisecond to store this page in cache 42 | ttl: 1000 * 60 // 1 Minute 43 | }, 44 | pages: [ 45 | // these are prefixes of pages that need to be cached, for caching homepage use '/' 46 | '/page1', 47 | '/page2', 48 | 49 | ], 50 | key: (route: string, headers: any, device: Device) => { 51 | // Custom function to return cache key 52 | // return false to bypass cache 53 | 54 | } 55 | } 56 | ], 57 | ], 58 | 59 | // ... 60 | }) 61 | ``` 62 | 63 | ## Configuration 64 | 65 | | Option | Type | Required | Description | Default | 66 | | ------ | ---- | ------ | ----------- | ------- | 67 | | enabled | `boolean` | No |To enable/ disable the SSR cache | `true` | 68 | | store | `object` | No | SSR cache store options | `{type:'',max:500,ttl:10000}` | 69 | | pages | `Array` | Yes |Pages to cache | N/A | 70 | | key | `Function` | No | Use for generating custo key based on route,headers,and device type. Returned string will be hashed using `ohash`. return false to bypass cache | `url` | 71 | |||||| 72 | 73 | 74 | ## Device Interface 75 | ```javascript 76 | interface Device { 77 | userAgent: string 78 | isDesktop: boolean 79 | isIos: boolean 80 | isAndroid: boolean 81 | isMobile: boolean 82 | isMobileOrTablet: boolean 83 | isDesktopOrTablet: boolean 84 | isTablet: boolean 85 | isWindows: boolean 86 | isMacOS: boolean 87 | isApple: boolean 88 | isSafari: boolean 89 | isFirefox: boolean 90 | isEdge: boolean 91 | isChrome: boolean 92 | isSamsung: boolean 93 | isCrawler: boolean 94 | } 95 | ``` 96 | ## caveat 97 | **important security warning** : don't load secret keys such as user credential on the server for cached pages. 98 | _this is because they will cache for all users!_ 99 | 100 | 101 | 102 | [npm-downloads-src]: https://img.shields.io/npm/dt/nuxt-cache-ssr 103 | [npm-downloads-href]: https://npmjs.com/package/nuxt-cache-ssr 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-cache-ssr", 3 | "version": "1.1.0", 4 | "license": "MIT", 5 | "type": "module", 6 | "author": { 7 | "name": "Bhaskar Gyan Vardhan" 8 | }, 9 | "exports": { 10 | ".": { 11 | "import": "./dist/module.mjs", 12 | "require": "./dist/module.cjs" 13 | }, 14 | "./dist/runtime/*": "./dist/runtime/*.mjs", 15 | "./*": "./*" 16 | }, 17 | "main": "./dist/module.cjs", 18 | "types": "./dist/types.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "prepack": "nuxt-module-build", 24 | "dev": "nuxi dev playground", 25 | "dev:build": "nuxi build playground", 26 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground" 27 | }, 28 | "dependencies": { 29 | "cache-manager": "^5.1.1", 30 | "ohash": "^0.1.5" 31 | }, 32 | "devDependencies": { 33 | "@nuxt/kit": "^3.0.0-rc.12", 34 | "@nuxt/module-builder": "^0.2.0", 35 | "@nuxt/schema": "^3.0.0-rc.12", 36 | "@nuxtjs/eslint-config-typescript": "^11.0.0", 37 | "eslint": "^8.25.0", 38 | "nuxt": "^3.0.0-rc.12" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git@github.com:bhaskarGyan/nuxt-cache-ssr.git" 43 | }, 44 | "keywords": [ 45 | "nuxt", 46 | "nuxt3", 47 | "ssr", 48 | "cache", 49 | "redis", 50 | "vue3", 51 | "spa", 52 | "nuxt-module" 53 | ], 54 | "bugs": { 55 | "url": "https://github.com/bhaskarGyan/nuxt-cache-ssr/issues" 56 | }, 57 | "homepage": "https://github.com/bhaskarGyan/nuxt-cache-ssr#readme" 58 | } 59 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import MyModule from '..' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [ 6 | MyModule 7 | ], 8 | myModule: { 9 | addPlugin: true 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-module-playground" 4 | } 5 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule, createResolver, addServerHandler } from '@nuxt/kit' 2 | export interface ModuleOptions { 3 | enabled: boolean 4 | } 5 | 6 | export default defineNuxtModule({ 7 | meta: { 8 | name: 'nuxt-cache-ssr', 9 | configKey: 'cacheSSR', 10 | compatibility: { 11 | nuxt: '^3.0.0-rc.9' 12 | }, 13 | default: { 14 | enabled: true 15 | } 16 | }, 17 | async setup(options, nuxt) { 18 | if (!options.enabled) return 19 | const { resolve } = createResolver(import.meta.url) 20 | const runtimeDir = await resolve('./runtime') 21 | nuxt.options.build.transpile.push(runtimeDir) 22 | 23 | nuxt.hook('nitro:config', (nitro) => { 24 | nitro.externals = nitro.externals || {} 25 | nitro.externals.inline = nitro.externals.inline || [] 26 | nitro.externals.inline.push(runtimeDir) 27 | nitro.virtual = nitro.virtual || {} 28 | nitro.virtual['#cache-ssr-options'] = `export const options = ${JSON.stringify(options, function (key, val) { 29 | if (typeof val === 'function') { 30 | return val + ''; 31 | } 32 | return val; 33 | }, 2)}` 34 | nitro.plugins = nitro.plugins || [] 35 | nitro.plugins.push(resolve('runtime/cache.nitro')) 36 | }) 37 | 38 | addServerHandler({ 39 | handler: resolve("runtime/cache.middleware") 40 | }) 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /src/runtime/cache.inmemory.ts: -------------------------------------------------------------------------------- 1 | // import LRU from 'lru-cache'; 2 | import { caching } from 'cache-manager' 3 | import { hash } from 'ohash'; 4 | import { options } from '#cache-ssr-options' 5 | 6 | const DefaultCacheOption = { 7 | max: 500, 8 | ttl: 10000 9 | } 10 | interface CacheOptions { 11 | max: number; 12 | ttl: number; 13 | } 14 | const DefaultOptionsLRU = { 15 | max: 500, 16 | ttl: 1000 * 60, 17 | } 18 | interface InMemory { 19 | initialized: boolean 20 | } 21 | class InMemoryCache { 22 | cached: any; 23 | 24 | constructor(options = {}) { 25 | this.cached = {} 26 | } 27 | 28 | async get(key: string) { 29 | 30 | const result = await this.cached.get(hash(key)) 31 | return result 32 | } 33 | 34 | async set(key: string, value: any) { 35 | 36 | await this.cached.set(hash(key), value) 37 | } 38 | 39 | async init(options: CacheOptions) { 40 | let cachingOption: CacheOptions = DefaultCacheOption; 41 | if (options !== null && typeof options === 'object') { 42 | cachingOption = { ...cachingOption, ...options } 43 | } 44 | this.cached = await caching('memory', cachingOption) 45 | } 46 | 47 | } 48 | 49 | 50 | 51 | export default new InMemoryCache() -------------------------------------------------------------------------------- /src/runtime/cache.middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import InMemoryCache from './cache.inmemory' 3 | import { options } from '#cache-ssr-options' 4 | import { isUrlCacheable } from './cache.utils' 5 | import { fromNodeMiddleware } from 'h3' 6 | import generateFlags from './generateFlags' 7 | 8 | const customKey = options.key ? eval(options.key) : null 9 | 10 | export default fromNodeMiddleware(async (req, res, next) => { 11 | const { url } = req 12 | 13 | if (isUrlCacheable(req, res, options.pages)) { 14 | const key = customKey ? customKey(url, req.headers, generateFlags(req.headers, req.headers['user-agent'])) : url; 15 | 16 | if (key && typeof key === 'string') { 17 | 18 | const cachedRes = await InMemoryCache.get(key); 19 | 20 | if (cachedRes) { 21 | res.writeHead(200, { ...cachedRes.headers, 'x-ssr-cache': 'HIT' }); 22 | res.end(cachedRes.body) 23 | } else { 24 | res.setHeader('x-ssr-cache', 'MISS') 25 | } 26 | } 27 | } 28 | }) -------------------------------------------------------------------------------- /src/runtime/cache.nitro.ts: -------------------------------------------------------------------------------- 1 | import InMemoryCache from './cache.inmemory' 2 | import zlib from "node:zlib"; 3 | import { options } from '#cache-ssr-options' 4 | import { isUrlCacheable } from './cache.utils' 5 | import type { NitroAppPlugin } from 'nitropack' 6 | import generateFlags from './generateFlags' 7 | 8 | const customKey = options.key ? eval(options.key) : null 9 | 10 | const gzipOptions = { level: zlib.constants.Z_BEST_COMPRESSION } 11 | const brotliOptions = { 12 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, 13 | [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.Z_BEST_COMPRESSION, 14 | } 15 | 16 | const { encoding = '' } = options.compressResponse || {} as any 17 | 18 | const compressedBuff = (fileContents: string) => { 19 | return new Promise((resolve, reject) => { 20 | const cb = (error, result?: Buffer) => error ? reject(error) : resolve(result) 21 | if (encoding === 'gzip') { 22 | zlib.gzip(fileContents, gzipOptions, cb) 23 | } else if (encoding === 'br') { 24 | zlib.brotliCompress(fileContents, brotliOptions, cb) 25 | } else { 26 | cb('Invalid compression option. Please provide either br or gzip') 27 | } 28 | }) 29 | } 30 | 31 | export default async function (nitroApp) { 32 | const cacheOption = options.store || {}; 33 | 34 | await InMemoryCache.init(cacheOption); 35 | 36 | nitroApp.hooks.hook('render:response', async (response, { event }) => { 37 | const isCacheable = isUrlCacheable(event.req, event.res, options.pages) 38 | 39 | if (isCacheable && response.statusCode === 200) { 40 | const key = customKey ? customKey(event.req.url, event.req.headers, generateFlags(event.req.headers, event.req.headers['user-agent'])) : event.req.url 41 | 42 | if (key && typeof key === 'string') { 43 | let cachedRes = response; 44 | 45 | if (encoding) { 46 | const encodedBuffer = await compressedBuff(response.body); 47 | 48 | cachedRes = { 49 | ...cachedRes, body: encodedBuffer, headers: { 50 | ...cachedRes.headers, "content-encoding": encoding 51 | } 52 | } 53 | } 54 | 55 | await InMemoryCache.set(key, cachedRes); 56 | } 57 | 58 | 59 | } 60 | 61 | }) 62 | } -------------------------------------------------------------------------------- /src/runtime/cache.utils.ts: -------------------------------------------------------------------------------- 1 | import { options } from '#cache-ssr-options' 2 | 3 | 4 | 5 | export const isUrlCacheable = (req, res, pages = []) => { 6 | const { disableCacheOnDemand = {} } = options; 7 | const { headerKey = '' } = disableCacheOnDemand; 8 | const { url } = req 9 | let isCacheable = false 10 | 11 | if (headerKey) { 12 | const resHeaders = res.getHeaders(); 13 | if (headerKey in resHeaders) { 14 | return false 15 | } 16 | } 17 | 18 | pages.forEach(page => { 19 | // caching for home page, Need to work on better logic 20 | if (page === '/') { 21 | if (page === url) { 22 | isCacheable = true 23 | } 24 | } else if (url?.startsWith(page)) { 25 | isCacheable = true 26 | } 27 | 28 | }); 29 | 30 | return isCacheable 31 | 32 | } -------------------------------------------------------------------------------- /src/runtime/generateFlags.ts: -------------------------------------------------------------------------------- 1 | /* Following regex originated from Nuxt device module 2 | * https://github.com/nuxt-community/device-module/blob/master/lib/plugin.js 3 | */ 4 | import type { Device } from './types' 5 | 6 | // eslint-disable-next-line 7 | const REGEX_MOBILE1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|FBAN|FBAV|fennec|hiptop|iemobile|ip(hone|od)|Instagram|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i 8 | 9 | // eslint-disable-next-line 10 | const REGEX_MOBILE2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i 11 | 12 | function isMobile(a: string): boolean { 13 | return REGEX_MOBILE1.test(a) || REGEX_MOBILE2.test(a.slice(0, 4)) 14 | } 15 | 16 | // eslint-disable-next-line 17 | const REGEX_MOBILE_OR_TABLET1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|FBAN|FBAV|fennec|hiptop|iemobile|ip(hone|od)|Instagram|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i 18 | // eslint-disable-next-line 19 | const REGEX_MOBILE_OR_TABLET2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i 20 | 21 | // eslint-disable-next-line 22 | const REGEX_CRAWLER = /Googlebot\/|Googlebot-Mobile|Googlebot-Image|Googlebot-News|Googlebot-Video|AdsBot-Google([^-]|$)|AdsBot-Google-Mobile|Feedfetcher-Google|Mediapartners-Google|Mediapartners \(Googlebot\)|APIs-Google|bingbot|Slurp|[wW]get|LinkedInBot|Python-urllib|python-requests|aiohttp|httpx|libwww-perl|httpunit|nutch|Go-http-client|phpcrawl|msnbot|jyxobot|FAST-WebCrawler|FAST Enterprise Crawler|BIGLOTRON|Teoma|convera|seekbot|Gigabot|Gigablast|exabot|ia_archiver|GingerCrawler|webmon |HTTrack|grub.org|UsineNouvelleCrawler|antibot|netresearchserver|speedy|fluffy|findlink|msrbot|panscient|yacybot|AISearchBot|ips-agent|tagoobot|MJ12bot|woriobot|yanga|buzzbot|mlbot|YandexBot|YandexImages|YandexAccessibilityBot|YandexMobileBot|YandexMetrika|YandexTurbo|YandexImageResizer|YandexVideo|YandexAdNet|YandexBlogs|YandexCalendar|YandexDirect|YandexFavicons|YaDirectFetcher|YandexForDomain|YandexMarket|YandexMedia|YandexMobileScreenShotBot|YandexNews|YandexOntoDB|YandexPagechecker|YandexPartner|YandexRCA|YandexSearchShop|YandexSitelinks|YandexSpravBot|YandexTracker|YandexVertis|YandexVerticals|YandexWebmaster|YandexScreenshotBot|purebot|Linguee Bot|CyberPatrol|voilabot|Baiduspider|citeseerxbot|spbot|twengabot|postrank|TurnitinBot|scribdbot|page2rss|sitebot|linkdex|Adidxbot|ezooms|dotbot|Mail.RU_Bot|discobot|heritrix|findthatfile|europarchive.org|NerdByNature.Bot|sistrix crawler|Ahrefs(Bot|SiteAudit)|fuelbot|CrunchBot|IndeedBot|mappydata|woobot|ZoominfoBot|PrivacyAwareBot|Multiviewbot|SWIMGBot|Grobbot|eright|Apercite|semanticbot|Aboundex|domaincrawler|wbsearchbot|summify|CCBot|edisterbot|seznambot|ec2linkfinder|gslfbot|aiHitBot|intelium_bot|facebookexternalhit|Yeti|RetrevoPageAnalyzer|lb-spider|Sogou|lssbot|careerbot|wotbox|wocbot|ichiro|DuckDuckBot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnam gnam spider|web-archive-net.com.bot|backlinkcrawler|coccoc|integromedb|content crawler spider|toplistbot|it2media-domain-crawler|ip-web-crawler.com|siteexplorer.info|elisabot|proximic|changedetection|arabot|WeSEE:Search|niki-bot|CrystalSemanticsBot|rogerbot|360Spider|psbot|InterfaxScanBot|CC Metadata Scaper|g00g1e.net|GrapeshotCrawler|urlappendbot|brainobot|fr-crawler|binlar|SimpleCrawler|Twitterbot|cXensebot|smtbot|bnf.fr_bot|A6-Indexer|ADmantX|Facebot|OrangeBot\/|memorybot|AdvBot|MegaIndex|SemanticScholarBot|ltx71|nerdybot|xovibot|BUbiNG|Qwantify|archive.org_bot|Applebot|TweetmemeBot|crawler4j|findxbot|S[eE][mM]rushBot|yoozBot|lipperhey|Y!J|Domain Re-Animator Bot|AddThis|Screaming Frog SEO Spider|MetaURI|Scrapy|Livelap[bB]ot|OpenHoseBot|CapsuleChecker|collection@infegy.com|IstellaBot|DeuSu\/|betaBot|Cliqzbot\/|MojeekBot\/|netEstate NE Crawler|SafeSearch microdata crawler|Gluten Free Crawler\/|Sonic|Sysomos|Trove|deadlinkchecker|Slack-ImgProxy|Embedly|RankActiveLinkBot|iskanie|SafeDNSBot|SkypeUriPreview|Veoozbot|Slackbot|redditbot|datagnionbot|Google-Adwords-Instant|adbeat_bot|WhatsApp|contxbot|pinterest.com.bot|electricmonk|GarlikCrawler|BingPreview\/|vebidoobot|FemtosearchBot|Yahoo Link Preview|MetaJobBot|DomainStatsBot|mindUpBot|Daum\/|Jugendschutzprogramm-Crawler|Xenu Link Sleuth|Pcore-HTTP|moatbot|KosmioBot|pingdom|AppInsights|PhantomJS|Gowikibot|PiplBot|Discordbot|TelegramBot|Jetslide|newsharecounts|James BOT|Bark[rR]owler|TinEye|SocialRankIOBot|trendictionbot|Ocarinabot|epicbot|Primalbot|DuckDuckGo-Favicons-Bot|GnowitNewsbot|Leikibot|LinkArchiver|YaK\/|PaperLiBot|Digg Deeper|dcrawl|Snacktory|AndersPinkBot|Fyrebot|EveryoneSocialBot|Mediatoolkitbot|Luminator-robots|ExtLinksBot|SurveyBot|NING\/|okhttp|Nuzzel|omgili|PocketParser|YisouSpider|um-LN|ToutiaoSpider|MuckRack|Jamie's Spider|AHC\/|NetcraftSurveyAgent|Laserlikebot|^Apache-HttpClient|AppEngine-Google|Jetty|Upflow|Thinklab|Traackr.com|Twurly|Mastodon|http_get|DnyzBot|botify|007ac9 Crawler|BehloolBot|BrandVerity|check_http|BDCbot|ZumBot|EZID|ICC-Crawler|ArchiveBot|^LCC |filterdb.iss.net\/crawler|BLP_bbot|BomboraBot|Buck\/|Companybook-Crawler|Genieo|magpie-crawler|MeltwaterNews|Moreover|newspaper\/|ScoutJet|(^| )sentry\/|StorygizeBot|UptimeRobot|OutclicksBot|seoscanners|Hatena|Google Web Preview|MauiBot|AlphaBot|SBL-BOT|IAS crawler|adscanner|Netvibes|acapbot|Baidu-YunGuanCe|bitlybot|blogmuraBot|Bot.AraTurka.com|bot-pge.chlooe.com|BoxcarBot|BTWebClient|ContextAd Bot|Digincore bot|Disqus|Feedly|Fetch\/|Fever|Flamingo_SearchEngine|FlipboardProxy|g2reader-bot|G2 Web Services|imrbot|K7MLWCBot|Kemvibot|Landau-Media-Spider|linkapediabot|vkShare|Siteimprove.com|BLEXBot\/|DareBoost|ZuperlistBot\/|Miniflux\/|Feedspot|Diffbot\/|SEOkicks|tracemyfile|Nimbostratus-Bot|zgrab|PR-CY.RU|AdsTxtCrawler|Datafeedwatch|Zabbix|TangibleeBot|google-xrawler|axios|Amazon CloudFront|Pulsepoint|CloudFlare-AlwaysOnline|Google-Structured-Data-Testing-Tool|WordupInfoSearch|WebDataStats|HttpUrlConnection|Seekport Crawler|ZoomBot|VelenPublicWebCrawler|MoodleBot|jpg-newsbot|outbrain|W3C_Validator|Validator\.nu|W3C-checklink|W3C-mobileOK|W3C_I18n-Checker|FeedValidator|W3C_CSS_Validator|W3C_Unicorn|Google-PhysicalWeb|Blackboard|ICBot\/|BazQux|Twingly|Rivva|Experibot|awesomecrawler|Dataprovider.com|GroupHigh\/|theoldreader.com|AnyEvent|Uptimebot\.org|Nmap Scripting Engine|2ip.ru|Clickagy|Caliperbot|MBCrawler|online-webceo-bot|B2B Bot|AddSearchBot|Google Favicon|HubSpot|Chrome-Lighthouse|HeadlessChrome|CheckMarkNetwork\/|www\.uptime\.com|Streamline3Bot\/|serpstatbot\/|MixnodeCache\/|^curl|SimpleScraper|RSSingBot|Jooblebot|fedoraplanet|Friendica|NextCloud|Tiny Tiny RSS|RegionStuttgartBot|Bytespider|Datanyze|Google-Site-Verification|TrendsmapResolver|tweetedtimes|NTENTbot|Gwene|SimplePie|SearchAtlas|Superfeedr|feedbot|UT-Dorkbot|Amazonbot|SerendeputyBot|Eyeotabot|officestorebot|Neticle Crawler|SurdotlyBot|LinkisBot|AwarioSmartBot|AwarioRssBot|RyteBot|FreeWebMonitoring SiteChecker|AspiegelBot|NAVER Blog Rssbot|zenback bot|SentiBot|Domains Project\/|Pandalytics|VKRobot|bidswitchbot|tigerbot|NIXStatsbot|Atom Feed Robot|Curebot|PagePeeker\/|Vigil\/|rssbot\/|startmebot\/|JobboerseBot|seewithkids|NINJA bot|Cutbot|BublupBot|BrandONbot|RidderBot|Taboolabot|Dubbotbot|FindITAnswersbot|infoobot|Refindbot|BlogTraffic\/\d\.\d+ Feed-Fetcher|SeobilityBot|Cincraw|Dragonbot|VoluumDSP-content-bot|FreshRSS|BitBot|^PHP-Curl-Class|Google-Certificates-Bridge/ 23 | 24 | function isMobileOrTablet(a: string): boolean { 25 | return REGEX_MOBILE_OR_TABLET1.test(a) || REGEX_MOBILE_OR_TABLET2.test(a.slice(0, 4)) 26 | } 27 | 28 | function isIos(a: string): boolean { 29 | return /iPad|iPhone|iPod/.test(a) 30 | } 31 | 32 | function isAndroid(a: string): boolean { 33 | return /android/i.test(a) 34 | } 35 | 36 | function isWindows(a: string): boolean { 37 | return /Windows/.test(a) 38 | } 39 | 40 | function isMacOS(a: string): boolean { 41 | return /Mac OS X/.test(a) 42 | } 43 | 44 | // Following regular expressions are originated from bowser(https://github.com/lancedikson/bowser). 45 | // Copyright 2015, Dustin Diaz (the "Original Author") 46 | // https://github.com/lancedikson/bowser/blob/master/LICENSE 47 | const browsers = [ 48 | { name: 'Samsung', test: /SamsungBrowser/i }, 49 | { name: 'Edge', test: /edg([ea]|ios|)\//i }, 50 | { name: 'Firefox', test: /firefox|iceweasel|fxios/i }, 51 | { name: 'Chrome', test: /chrome|crios|crmo/i }, 52 | { name: 'Safari', test: /safari|applewebkit/i } 53 | ] 54 | 55 | function getBrowserName(a: string): string { 56 | for (const b of browsers) { 57 | if (b.test.test(a)) { 58 | return b.name 59 | } 60 | } 61 | return '' 62 | } 63 | 64 | export default function generateFlags(headers, userAgent): Device { 65 | let mobile = false 66 | let mobileOrTablet = false 67 | let ios = false 68 | let android = false 69 | 70 | if (userAgent === 'Amazon CloudFront') { 71 | if (headers['cloudfront-is-mobile-viewer'] === 'true') { 72 | mobile = true 73 | mobileOrTablet = true 74 | } 75 | if (headers['cloudfront-is-tablet-viewer'] === 'true') { 76 | mobile = false 77 | mobileOrTablet = true 78 | } 79 | } else if (headers && headers['cf-device-type']) { // Cloudflare 80 | switch (headers['cf-device-type']) { 81 | case 'mobile': 82 | mobile = true 83 | mobileOrTablet = true 84 | break 85 | case 'tablet': 86 | mobile = false 87 | mobileOrTablet = true 88 | break 89 | case 'desktop': 90 | mobile = false 91 | mobileOrTablet = false 92 | break 93 | } 94 | } else { 95 | mobile = isMobile(userAgent) 96 | mobileOrTablet = isMobileOrTablet(userAgent) 97 | ios = isIos(userAgent) 98 | android = isAndroid(userAgent) 99 | } 100 | const windows = isWindows(userAgent) 101 | const macOS = isMacOS(userAgent) 102 | const browserName = getBrowserName(userAgent) 103 | const isSafari = browserName === 'Safari' 104 | const isFirefox = browserName === 'Firefox' 105 | const isEdge = browserName === 'Edge' 106 | const isChrome = browserName === 'Chrome' 107 | const isSamsung = browserName === 'Samsung' 108 | const isCrawler = REGEX_CRAWLER.test(userAgent) 109 | 110 | return { 111 | userAgent, 112 | isMobile: mobile, 113 | isMobileOrTablet: mobileOrTablet, 114 | isTablet: !mobile && mobileOrTablet, 115 | isDesktop: !mobileOrTablet, 116 | isIos: ios, 117 | isAndroid: android, 118 | isWindows: windows, 119 | isMacOS: macOS, 120 | isApple: macOS || ios, 121 | isDesktopOrTablet: !mobile, 122 | isSafari, 123 | isFirefox, 124 | isEdge, 125 | isChrome, 126 | isSamsung, 127 | isCrawler 128 | } 129 | } -------------------------------------------------------------------------------- /src/runtime/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Device = { 2 | userAgent: string 3 | isDesktop: boolean 4 | isIos: boolean 5 | isAndroid: boolean 6 | isMobile: boolean 7 | isMobileOrTablet: boolean 8 | isDesktopOrTablet: boolean 9 | isTablet: boolean 10 | isWindows: boolean 11 | isMacOS: boolean 12 | isApple: boolean 13 | isSafari: boolean 14 | isFirefox: boolean 15 | isEdge: boolean 16 | isChrome: boolean 17 | isSamsung: boolean 18 | isCrawler: boolean 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------