├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ ├── pkg.pr.new.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── app.ts ├── app │ └── middleware │ │ └── static.ts ├── config │ ├── config.default.ts │ └── config.prod.ts ├── index.ts ├── types.ts └── typings │ └── index.d.ts ├── test ├── fixtures │ ├── static-server-custom │ │ ├── app │ │ │ └── assets │ │ │ │ └── foo.js │ │ ├── config │ │ │ └── config.default.js │ │ ├── dist │ │ │ └── static │ │ │ │ └── app │ │ │ │ └── assets │ │ │ │ └── foo-a1eb2031.js │ │ └── package.json │ ├── static-server-dist │ │ ├── app │ │ │ └── assets │ │ │ │ └── foo.js │ │ ├── config │ │ │ ├── config.default.js │ │ │ └── plugin.js │ │ ├── dist │ │ │ └── static │ │ │ │ └── app │ │ │ │ └── assets │ │ │ │ └── foo-a1eb2031.js │ │ └── package.json │ ├── static-server-with-dir │ │ ├── app │ │ │ └── public │ │ │ │ └── foo.js │ │ ├── config │ │ │ ├── config.default.js │ │ │ └── plugin.js │ │ ├── dist │ │ │ └── static │ │ │ │ └── app │ │ │ │ └── assets │ │ │ │ └── foo-a1eb2031.js │ │ └── package.json │ ├── static-server-with-dirs │ │ ├── app │ │ │ └── public │ │ │ │ └── foo.js │ │ ├── config │ │ │ ├── config.default.js │ │ │ └── plugin.js │ │ ├── dist │ │ │ └── static │ │ │ │ └── app │ │ │ │ └── assets │ │ │ │ └── foo-a1eb2031.js │ │ └── package.json │ └── static-server │ │ ├── app │ │ ├── controller │ │ │ └── foo.js │ │ ├── public │ │ │ └── foo.js │ │ └── router.js │ │ ├── config │ │ └── config.default.js │ │ └── package.json └── static.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | version: '18.19.0, 20, 22' 15 | secrets: 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/pkg.pr.new.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - run: corepack enable 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Build 21 | run: npm run prepublishOnly --if-present 22 | 23 | - run: npx pkg-pr-new publish 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: eggjs/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | test/fixtures/**/run 6 | .DS_Store 7 | .tshy* 8 | .eslintcache 9 | dist 10 | package-lock.json 11 | .package-lock.json 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.0.0](https://github.com/eggjs/static/compare/v2.3.1...v3.0.0) (2025-01-12) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * drop Node.js < 18.19.0 support 9 | 10 | part of https://github.com/eggjs/egg/issues/3644 11 | 12 | https://github.com/eggjs/egg/issues/5257 13 | 14 | 16 | 17 | ## Summary by CodeRabbit 18 | 19 | - **New Features** 20 | - Updated package to `@eggjs/static` 21 | - Enhanced TypeScript support 22 | - Improved static file serving configuration 23 | 24 | - **Chores** 25 | - Updated GitHub Actions workflows 26 | - Modernized project configuration 27 | - Updated Node.js version support to 18.19.0, 20, and 22 28 | 29 | - **Documentation** 30 | - Updated README with new package details 31 | - Simplified changelog and documentation 32 | 33 | - **Refactor** 34 | - Migrated from CommonJS to ES modules 35 | - Restructured project file organization 36 | 37 | 38 | 39 | ### Features 40 | 41 | * support cjs and esm both by tshy ([#26](https://github.com/eggjs/static/issues/26)) ([ab7d6fb](https://github.com/eggjs/static/commit/ab7d6fbed5c215febecca8a5f1a5b8e5515d99a2)) 42 | 43 | ## [2.3.1](https://github.com/eggjs/egg-static/compare/v2.3.0...v2.3.1) (2023-02-13) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * ignore mkdir if exists ([#24](https://github.com/eggjs/egg-static/issues/24)) ([92f3c33](https://github.com/eggjs/egg-static/commit/92f3c339592f789b2107aa849cfa0641df18ca12)) 49 | 50 | ## [2.3.0](https://github.com/eggjs/egg-static/compare/v2.2.0...v2.3.0) (2023-02-12) 51 | 52 | 53 | ### Features 54 | 55 | * add contributors ([3bf1ba1](https://github.com/eggjs/egg-static/commit/3bf1ba1b6bafd4b1a61b9fb0438c4ec07939af37)) 56 | 57 | --- 58 | 59 | 60 | 2.2.0 / 2019-02-15 61 | ================== 62 | 63 | **features** 64 | * [[`7a4b927`](http://github.com/eggjs/egg-static/commit/7a4b927e53670af89005fde057c838825fe96a30)] - feat: add options.dir for support multi folder serve. (#17) (仙森 <>) 65 | 66 | 2.1.1 / 2018-05-02 67 | ================== 68 | 69 | **fixes** 70 | * [[`a55f7ad`](http://github.com/eggjs/egg-static/commit/a55f7ad50ab880f3114bf12910f5f64e1d4da941)] - fix: range only work with static prefix url (#15) (Yiyu He <>) 71 | 72 | 2.1.0 / 2018-01-10 73 | ================== 74 | 75 | **features** 76 | * [[`cd35dea`](http://github.com/eggjs/egg-static/commit/cd35dea2ccf98dc7fed7d36a25f5555f3712eb8f)] - feat: add range support (#13) (HelloYou <>) 77 | 78 | **others** 79 | * [[`93a56c1`](http://github.com/eggjs/egg-static/commit/93a56c1af60c69cd814d33696224a7f044034da6)] - docs: fix confusion for option:prefix (#12) (Airyland <>) 80 | 81 | 2.0.0 / 2017-11-09 82 | ================== 83 | 84 | **others** 85 | * [[`bc2d05c`](http://github.com/eggjs/egg-static/commit/bc2d05c10fe6aabc3e0190a20866dd45f4134dda)] - refactor: upgrade dependencies and support egg@2 (#11) (Yiyu He <>) 86 | * [[`779e4fa`](http://github.com/eggjs/egg-static/commit/779e4fa7d171fa7e1c51c902e9b47be9632cb35d)] - docs: update usage (#10) (TZ | 天猪 <>) 87 | 88 | 1.4.1 / 2017-06-04 89 | ================== 90 | 91 | * docs: fix License url (#9) 92 | 93 | 1.4.0 / 2017-06-01 94 | ================== 95 | 96 | * feat: use lru to store files (#8) 97 | 98 | 1.3.0 / 2017-03-25 99 | ================== 100 | 101 | * feat: add support multiple directory (#7) 102 | 103 | 1.2.0 / 2017-02-21 104 | ================== 105 | 106 | * deps: upgrade koa-static-cache to 4.x (#6) 107 | * chore: upgrade deps and fix test (#5) 108 | 109 | 1.1.0 / 2017-01-13 110 | ================== 111 | 112 | * feat: default lazyload (#4) 113 | * docs: note for koa-static-cache (#3) 114 | 115 | 1.0.0 / 2016-11-02 116 | ================== 117 | 118 | * test: add node v7 (#2) 119 | 120 | 0.1.0 / 2016-07-18 121 | ================== 122 | 123 | * test: add tests (#1) 124 | 125 | 0.0.2 / 2016-07-15 126 | ================== 127 | 128 | * init 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. 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 | # @eggjs/static 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Node.js CI](https://github.com/eggjs/static/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/static/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-image]][snyk-url] 7 | [![npm download][download-image]][download-url] 8 | [![Node.js Version](https://img.shields.io/node/v/@eggjs/static.svg?style=flat)](https://nodejs.org/en/download/) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) 10 | 11 | [npm-image]: https://img.shields.io/npm/v/@eggjs/static.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/@eggjs/static 13 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/static.svg?style=flat-square 14 | [codecov-url]: https://codecov.io/github/eggjs/static?branch=master 15 | [snyk-image]: https://snyk.io/test/npm/@eggjs/static/badge.svg?style=flat-square 16 | [snyk-url]: https://snyk.io/test/npm/@eggjs/static 17 | [download-image]: https://img.shields.io/npm/dm/@eggjs/static.svg?style=flat-square 18 | [download-url]: https://npmjs.org/package/@eggjs/static 19 | 20 | Static server plugin for egg, base on [@eggjs/koa-static-cache](https://github.com/eggjs/koa-static-cache). 21 | 22 | ## Install 23 | 24 | `@eggjs/static` is a plugin that has been built-in for egg. It is enabled by default. 25 | 26 | ## Configuration 27 | 28 | `@eggjs/static` support all configurations in [@eggjs/koa-static-cache](https://github.com/eggjs/koa-static-cache). 29 | And with default configurations below: 30 | 31 | - prefix: `'/public/'` 32 | - dir: `path.join(appInfo.baseDir, 'app/public')` 33 | - dynamic: `true` 34 | - preload: `false` 35 | - maxAge: `31536000` in prod env, `0` in other envs 36 | - buffer: `true` in prod env, `false` in other envs 37 | 38 | `@eggjs/static` provides one more option: 39 | 40 | - maxFiles: the maximum value of cache items, only effective when dynamic is true, default is `1000`. 41 | 42 | **All static files in `$baseDir/app/public` can be visited with prefix `/public`, and all the files are lazy loaded.** 43 | 44 | - In non-production environment, assets won't be cached, your modification can take effect immediately. 45 | - In production environment, `@eggjs/static` will cache the assets after visited, you need to restart the process to update the assets. 46 | - Dir default is `$baseDir/app/public` but you can also define **multiple directory** by use `dir: [dir1, dir2, ...]` or `dir: [dir1, { prefix: '/static2', dir: dir2 }]`, static server will use all these directories. 47 | 48 | ```ts 49 | // {app_root}/config/config.default.ts 50 | export default { 51 | static: { 52 | // maxAge: 31536000, 53 | }, 54 | }; 55 | ``` 56 | 57 | ## Questions & Suggestions 58 | 59 | Please open an issue [here](https://github.com/eggjs/egg/issues). 60 | 61 | ## License 62 | 63 | [MIT](LICENSE) 64 | 65 | ## Contributors 66 | 67 | [![Contributors](https://contrib.rocks/image?repo=eggjs/static)](https://github.com/eggjs/static/graphs/contributors) 68 | 69 | Made with [contributors-img](https://contrib.rocks). 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eggjs/static", 3 | "version": "3.0.0", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "static server plugin for egg", 8 | "eggPlugin": { 9 | "name": "static", 10 | "exports": { 11 | "import": "./dist/esm", 12 | "require": "./dist/commonjs", 13 | "typescript": "./src" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/eggjs/static.git" 19 | }, 20 | "keywords": [ 21 | "egg", 22 | "egg-plugin", 23 | "eggPlugin", 24 | "static" 25 | ], 26 | "author": "dead_horse", 27 | "license": "MIT", 28 | "engines": { 29 | "node": ">= 18.19.0" 30 | }, 31 | "dependencies": { 32 | "@eggjs/core": "^6.2.13", 33 | "@eggjs/koa-static-cache": "^6.0.0", 34 | "is-type-of": "^2.2.0", 35 | "koa-compose": "^4.1.0", 36 | "koa-range": "^0.3.0", 37 | "ylru": "^2.0.0" 38 | }, 39 | "devDependencies": { 40 | "@arethetypeswrong/cli": "^0.17.1", 41 | "@eggjs/bin": "7", 42 | "@eggjs/mock": "^6.0.5", 43 | "@eggjs/tsconfig": "1", 44 | "@types/koa-compose": "^3.2.8", 45 | "@types/koa-range": "^0.3.5", 46 | "@types/mocha": "10", 47 | "@types/node": "22", 48 | "egg": "4", 49 | "eslint": "8", 50 | "eslint-config-egg": "14", 51 | "rimraf": "6", 52 | "tshy": "3", 53 | "tshy-after": "1", 54 | "typescript": "5" 55 | }, 56 | "scripts": { 57 | "lint": "eslint --cache src test --ext .ts", 58 | "pretest": "npm run clean && npm run lint -- --fix", 59 | "test": "egg-bin test", 60 | "preci": "npm run clean && npm run lint", 61 | "ci": "egg-bin cov", 62 | "postci": "npm run prepublishOnly && npm run clean", 63 | "clean": "rimraf dist", 64 | "prepublishOnly": "tshy && tshy-after && attw --pack" 65 | }, 66 | "type": "module", 67 | "tshy": { 68 | "exports": { 69 | ".": "./src/index.ts", 70 | "./package.json": "./package.json" 71 | } 72 | }, 73 | "exports": { 74 | ".": { 75 | "import": { 76 | "types": "./dist/esm/index.d.ts", 77 | "default": "./dist/esm/index.js" 78 | }, 79 | "require": { 80 | "types": "./dist/commonjs/index.d.ts", 81 | "default": "./dist/commonjs/index.js" 82 | } 83 | }, 84 | "./package.json": "./package.json" 85 | }, 86 | "files": [ 87 | "dist", 88 | "src" 89 | ], 90 | "types": "./dist/commonjs/index.d.ts", 91 | "main": "./dist/commonjs/index.js", 92 | "module": "./dist/esm/index.js" 93 | } 94 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import type { ILifecycleBoot, EggCore } from '@eggjs/core'; 2 | 3 | export default class AppBoot implements ILifecycleBoot { 4 | private readonly app; 5 | constructor(app: EggCore) { 6 | this.app = app; 7 | } 8 | async configWillLoad() { 9 | const app = this.app; 10 | // make sure static middleware is before bodyParser 11 | const index = app.config.coreMiddleware.indexOf('bodyParser'); 12 | if (index === -1) { 13 | app.config.coreMiddleware.push('static'); 14 | } else { 15 | app.config.coreMiddleware.splice(index, 0, 'static'); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/middleware/static.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { mkdirSync, existsSync } from 'node:fs'; 3 | import range from 'koa-range'; 4 | import compose from 'koa-compose'; 5 | import type { EggCore, Context, Next } from '@eggjs/core'; 6 | import { staticCache } from '@eggjs/koa-static-cache'; 7 | import { LRU } from 'ylru'; 8 | import { isObject } from 'is-type-of'; 9 | import type { StaticConfig, StaticDirOptions } from '../../types.js'; 10 | 11 | export default (options: StaticConfig, app: EggCore) => { 12 | const dirs = (options.dirs ?? []).concat(options.dir); 13 | 14 | const prefixes: string[] = []; 15 | 16 | function rangeMiddleware(ctx: Context, next: Next) { 17 | // if match static file, and use range middleware. 18 | const isMatch = prefixes.some(p => ctx.path.startsWith(p)); 19 | if (isMatch) { 20 | return range(ctx as any, next); 21 | } 22 | return next(); 23 | } 24 | 25 | const middlewares = [ rangeMiddleware ]; 26 | 27 | for (const dirObj of dirs) { 28 | assert(isObject(dirObj) || typeof dirObj === 'string', 29 | '`config.static.dir` must be `string | Array`'); 30 | 31 | let newOptions: StaticDirOptions; 32 | if (typeof dirObj === 'string') { 33 | // copy origin options to new options ensure the safety of objects 34 | newOptions = { 35 | ...options, 36 | dir: dirObj, 37 | }; 38 | } else { 39 | assert(typeof dirObj.dir === 'string', 40 | '`config.static.dirs` should contains `[].dir` property when object style'); 41 | newOptions = { 42 | ...options, 43 | ...dirObj, 44 | }; 45 | } 46 | 47 | if (newOptions.dynamic && !newOptions.files) { 48 | newOptions.files = new LRU(newOptions.maxFiles); 49 | } 50 | 51 | if (newOptions.prefix) { 52 | prefixes.push(newOptions.prefix); 53 | } 54 | 55 | // ensure directory exists 56 | if (!existsSync(newOptions.dir)) { 57 | mkdirSync(newOptions.dir, { recursive: true }); 58 | } 59 | middlewares.push(staticCache(newOptions)); 60 | 61 | (app as any).coreLogger.info('[@eggjs/static] starting static serve %s -> %s', 62 | newOptions.prefix, newOptions.dir); 63 | } 64 | 65 | return compose(middlewares); 66 | }; 67 | -------------------------------------------------------------------------------- /src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type { EggAppInfo } from '@eggjs/core'; 3 | import type { StaticConfig } from '../types.js'; 4 | 5 | export default (appInfo: EggAppInfo) => { 6 | return { 7 | static: { 8 | prefix: '/public/', 9 | dir: path.join(appInfo.baseDir, 'app/public'), 10 | // dirs: [ dir1, dir2 ] or [ dir1, { prefix: '/static2', dir: dir2 } ], 11 | dirs: undefined, 12 | // support lazy load 13 | dynamic: true, 14 | preload: false, 15 | buffer: false, 16 | maxFiles: 1000, 17 | } as StaticConfig, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import type { StaticConfig } from '../types.js'; 2 | 3 | export default { 4 | static: { 5 | maxAge: 31536000, 6 | buffer: true, 7 | } as StaticConfig, 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './types.js'; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Options as StaticCacheOptions } from '@eggjs/koa-static-cache'; 2 | 3 | export interface StaticDirOptions extends Omit { 4 | /** 5 | * static files store dir 6 | */ 7 | dir: string; 8 | /** 9 | * static files prefix 10 | * Default to `'/public/'` 11 | */ 12 | prefix: string; 13 | /** 14 | * cache max age in `seconds` 15 | * Default to `0` on development, `31536000` on production 16 | */ 17 | maxAge: number; 18 | /** 19 | * dynamic load file which not cached on initialization 20 | * Default to `true 21 | */ 22 | dynamic: boolean; 23 | /** 24 | * caches the assets on initialization or not, 25 | * always work together with `options.dynamic` 26 | * Default to `false` 27 | */ 28 | preload: boolean; 29 | /** 30 | * buffer the file content or not 31 | * Default to `false` on development, `true` on production 32 | */ 33 | buffer: boolean; 34 | /** 35 | * max files count in store 36 | * Default to `1000` 37 | */ 38 | maxFiles: number; 39 | } 40 | 41 | /** 42 | * Static file serve 43 | * @member Config#static 44 | * @see https://github.com/koajs/static-cache 45 | */ 46 | export interface StaticConfig extends Omit { 47 | /** 48 | * static files store dir 49 | * Default to `${baseDir}/app/public` 50 | */ 51 | dir: string | Array; 52 | /** 53 | * static files store dirs 54 | * @deprecated use `dir` instead 55 | */ 56 | dirs?: Array; 57 | } 58 | 59 | declare module '@eggjs/core' { 60 | // add EggAppConfig overrides types 61 | interface EggAppConfig { 62 | static: StaticConfig; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | // make sure to import egg typings and let typescript know about it 2 | // @see https://github.com/whxaxes/blog/issues/11 3 | // and https://www.typescriptlang.org/docs/handbook/declaration-merging.html 4 | import 'egg'; 5 | -------------------------------------------------------------------------------- /test/fixtures/static-server-custom/app/assets/foo.js: -------------------------------------------------------------------------------- 1 | alert('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-custom/config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = info => { 4 | const exports = {}; 5 | 6 | exports.static = { 7 | prefix: '/static-custom', 8 | dir: path.join(info.baseDir, 'dist/static') 9 | }; 10 | 11 | return exports; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/static-server-custom/dist/static/app/assets/foo-a1eb2031.js: -------------------------------------------------------------------------------- 1 | define("static/app/assets/foo-a1eb2031",[],function(a,e,t){alert("bar")}); -------------------------------------------------------------------------------- /test/fixtures/static-server-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/static-server-dist/app/assets/foo.js: -------------------------------------------------------------------------------- 1 | alert('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-dist/config/config.default.js: -------------------------------------------------------------------------------- 1 | module.exports = info => { 2 | return { 3 | static: { 4 | prefix: '/static', 5 | dir: info.baseDir + '/dist/static', 6 | buffer: true, 7 | } 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/static-server-dist/config/plugin.js: -------------------------------------------------------------------------------- 1 | exports.static = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-dist/dist/static/app/assets/foo-a1eb2031.js: -------------------------------------------------------------------------------- 1 | define("static/app/assets/foo-a1eb2031",[],function(a,e,t){alert("bar")}); -------------------------------------------------------------------------------- /test/fixtures/static-server-dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dir/app/public/foo.js: -------------------------------------------------------------------------------- 1 | console.log('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dir/config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = appInfo => { 4 | return { 5 | keys: 'aaa', 6 | static: { 7 | prefix: '/public', 8 | dir: [ 9 | path.join(appInfo.baseDir, '/app/public'), 10 | path.join(appInfo.baseDir, '/dist/static') 11 | ], 12 | buffer: true, 13 | } 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dir/config/plugin.js: -------------------------------------------------------------------------------- 1 | exports.static = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dir/dist/static/app/assets/foo-a1eb2031.js: -------------------------------------------------------------------------------- 1 | define("static/app/assets/foo-a1eb2031",[],function(a,e,t){alert("bar")}); -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dirs/app/public/foo.js: -------------------------------------------------------------------------------- 1 | console.log('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dirs/config/config.default.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = appInfo => { 4 | return { 5 | keys: 'aaa', 6 | static: { 7 | prefix: '/public', 8 | dirs: [ 9 | path.join(appInfo.baseDir, '/app/public'), 10 | { 11 | prefix: '/static', 12 | dir: path.join(appInfo.baseDir, '/dist/static'), 13 | } 14 | ], 15 | buffer: true, 16 | } 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dirs/config/plugin.js: -------------------------------------------------------------------------------- 1 | exports.static = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dirs/dist/static/app/assets/foo-a1eb2031.js: -------------------------------------------------------------------------------- 1 | define("static/app/assets/foo-a1eb2031",[],function(a,e,t){alert("bar")}); -------------------------------------------------------------------------------- /test/fixtures/static-server-with-dirs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/static-server/app/controller/foo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.bar = function(ctx) { 4 | ctx.body = 'hello world'; 5 | ctx.type = 'text'; 6 | ctx.status = 200; 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/static-server/app/public/foo.js: -------------------------------------------------------------------------------- 1 | console.log('bar'); 2 | -------------------------------------------------------------------------------- /test/fixtures/static-server/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | app.get('/foo/bar', 'foo.bar'); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/static-server/config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.keys = 'foo'; 4 | -------------------------------------------------------------------------------- /test/fixtures/static-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-server" 3 | } 4 | -------------------------------------------------------------------------------- /test/static.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import fs from 'node:fs/promises'; 4 | import { mock, MockApplication } from '@eggjs/mock'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | export function getFixtures(filename: string) { 10 | return path.join(__dirname, 'fixtures', filename); 11 | } 12 | 13 | describe('test/static.test.ts', () => { 14 | describe('serve public', () => { 15 | let app: MockApplication; 16 | before(() => { 17 | app = mock.app({ 18 | baseDir: 'static-server', 19 | }); 20 | return app.ready(); 21 | }); 22 | 23 | after(() => app.close()); 24 | 25 | it('should get exists js file', () => { 26 | return app.httpRequest() 27 | .get('/public/foo.js') 28 | .expect(/console.log\(\'bar\'\);[\r\n]/) 29 | .expect(200); 30 | }); 31 | 32 | it('should get /public 404', () => { 33 | return app.httpRequest() 34 | .get('/public') 35 | .expect(404); 36 | }); 37 | 38 | it('should 404', () => { 39 | return app.httpRequest() 40 | .get('/public/foo404.js') 41 | .expect(404); 42 | }); 43 | 44 | it('should return 206 with partial content', () => { 45 | return app.httpRequest() 46 | .get('/public/foo.js') 47 | .set('range', 'bytes=0-10') 48 | .expect('Content-Length', '11') 49 | .expect('Accept-Ranges', 'bytes') 50 | .expect('Content-Range', 51 | process.platform === 'win32' ? 'bytes 0-10/21' : 'bytes 0-10/20') 52 | .expect('console.log') 53 | .expect(206); 54 | }); 55 | 56 | it('should range don\'t effect non static router', () => { 57 | return app.httpRequest() 58 | .get('/foo/bar') 59 | .set('range', 'bytes=0-5') 60 | .expect('hello world') 61 | .expect(200); 62 | }); 63 | }); 64 | 65 | describe('serve dist', () => { 66 | let app: MockApplication; 67 | const jsFile: string = getFixtures('static-server-dist/dist/static/app/a.js'); 68 | before(async () => { 69 | await fs.writeFile(jsFile, 'console.log(\'a\')'); 70 | app = mock.app({ 71 | baseDir: 'static-server-dist', 72 | }); 73 | await app.ready(); 74 | }); 75 | 76 | after(() => app.close()); 77 | after(() => fs.unlink(jsFile)); 78 | 79 | it('should get js', () => { 80 | return app.httpRequest() 81 | .get('/static/app/assets/foo-a1eb2031.js') 82 | .expect(/define\("static\/app\/assets\/foo-a1eb2031"/) 83 | .expect(200); 84 | }); 85 | 86 | it('should cache file', async () => { 87 | await app.httpRequest() 88 | .get('/static/app/a.js') 89 | .expect('console.log(\'a\')') 90 | .expect(200); 91 | await fs.writeFile(jsFile, 'console.log(\'b\')'); 92 | await app.httpRequest() 93 | .get('/static/app/a.js') 94 | .expect('console.log(\'a\')') 95 | .expect(200); 96 | }); 97 | }); 98 | 99 | describe('serve custom using config.js', () => { 100 | let app: MockApplication; 101 | before(() => { 102 | app = mock.app({ 103 | baseDir: 'static-server-custom', 104 | }); 105 | return app.ready(); 106 | }); 107 | 108 | after(() => app.close()); 109 | 110 | it('should get js', () => { 111 | return app.httpRequest() 112 | .get('/static-custom/app/assets/foo-a1eb2031.js') 113 | .expect(/define\("static\/app\/assets\/foo-a1eb2031"/) 114 | .expect(200); 115 | }); 116 | }); 117 | 118 | describe('serve multiple folder with options.dir', () => { 119 | let app: MockApplication; 120 | const jsFile = getFixtures('static-server-with-dir/dist/static/app/a.js'); 121 | before(async () => { 122 | await fs.writeFile(jsFile, 'console.log(\'a\')'); 123 | app = mock.app({ 124 | baseDir: 'static-server-with-dir', 125 | }); 126 | await app.ready(); 127 | }); 128 | 129 | after(() => app.close()); 130 | after(() => fs.unlink(jsFile)); 131 | 132 | it('should get js correct from public folder', () => { 133 | return app.httpRequest() 134 | .get('/public/foo.js') 135 | .expect(/console.log\(\'bar\'\);[\r\n]/) 136 | .expect(200); 137 | }); 138 | 139 | it('should get js correct with range support', () => { 140 | return app.httpRequest() 141 | .get('/public/foo.js') 142 | .set('range', 'bytes=0-10') 143 | .expect('Content-Length', '11') 144 | .expect('Accept-Ranges', 'bytes') 145 | .expect('Content-Range', 146 | process.platform === 'win32' ? 'bytes 0-10/21' : 'bytes 0-10/20', 147 | ) 148 | .expect('console.log') 149 | .expect(206); 150 | }); 151 | 152 | it('should get js correct from dist folder', () => { 153 | return app.httpRequest() 154 | .get('/public/app/assets/foo-a1eb2031.js') 155 | .expect(/define\("static\/app\/assets\/foo-a1eb2031"/) 156 | .expect(200); 157 | }); 158 | 159 | it('should cache file', async () => { 160 | await app.httpRequest() 161 | .get('/public/app/a.js') 162 | .expect('console.log(\'a\')') 163 | .expect(200); 164 | await fs.writeFile(jsFile, 'console.log(\'b\')'); 165 | await app.httpRequest() 166 | .get('/public/app/a.js') 167 | .expect('console.log(\'a\')') 168 | .expect(200); 169 | }); 170 | }); 171 | 172 | describe('serve multiple folder with options.dirs', () => { 173 | let app: MockApplication; 174 | const jsFile = getFixtures('static-server-with-dirs/dist/static/app/a.js'); 175 | before(async () => { 176 | await fs.writeFile(jsFile, 'console.log(\'a\')'); 177 | app = mock.app({ 178 | baseDir: 'static-server-with-dirs', 179 | }); 180 | await app.ready(); 181 | }); 182 | 183 | after(() => app.close()); 184 | after(() => fs.unlink(jsFile)); 185 | 186 | it('should get js correct from public folder', () => { 187 | return app.httpRequest() 188 | .get('/public/foo.js') 189 | .expect(/console.log\(\'bar\'\);[\r\n]/) 190 | .expect(200); 191 | }); 192 | 193 | it('should get js correct with range support', () => { 194 | return app.httpRequest() 195 | .get('/public/foo.js') 196 | .set('range', 'bytes=0-10') 197 | .expect('Content-Length', '11') 198 | .expect('Accept-Ranges', 'bytes') 199 | .expect('Content-Range', 200 | process.platform === 'win32' ? 'bytes 0-10/21' : 'bytes 0-10/20', 201 | ) 202 | .expect('console.log') 203 | .expect(206); 204 | }); 205 | 206 | it('should get js correct from dist folder', () => { 207 | return app.httpRequest() 208 | .get('/static/app/assets/foo-a1eb2031.js') 209 | .expect(/define\("static\/app\/assets\/foo-a1eb2031"/) 210 | .expect(200); 211 | }); 212 | 213 | it('should cache file', async () => { 214 | await app.httpRequest() 215 | .get('/static/app/a.js') 216 | .expect('console.log(\'a\')') 217 | .expect(200); 218 | await fs.writeFile(jsFile, 'console.log(\'b\')'); 219 | await app.httpRequest() 220 | .get('/static/app/a.js') 221 | .expect('console.log(\'a\')') 222 | .expect(200); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | --------------------------------------------------------------------------------