├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs ├── cache.js ├── index.js └── package.json ├── esm ├── cache.js └── index.js ├── package.json ├── server.cjs └── test ├── api ├── hello.js └── package.json ├── index.html ├── index.js ├── package.json ├── server.cjs ├── source ├── archibold.jpg ├── benja-dark.svg ├── fa-regular-400.woff2 ├── favicon.ico ├── heresy.png ├── index.css ├── index.html ├── index.js ├── index.xml ├── package.json ├── rpi2.png ├── test.md ├── text.txt └── wiki-world.gif └── ucdn.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | test/dest/ 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µcdn 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/ucdn.svg?branch=master)](https://travis-ci.com/WebReflection/ucdn) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/ucdn/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/ucdn?branch=master) 4 | 5 | ![delivering packages](./test/ucdn.jpg) 6 | 7 | **Social Media Photo by [Eduardo Casajús Gorostiaga](https://unsplash.com/@eduardo_cg) on [Unsplash](https://unsplash.com/)** 8 | 9 | A [ucompress](https://github.com/WebReflection/ucompress#readme) based utility that accepts a configuration object with a `source` path, an optional `dest`, which fallbacks to the _temp_ folder, plus eventually extra `headers` property to pollute headers via `allow-origin` among other details. 10 | 11 | ### 📣 Community Announcement 12 | 13 | Please ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥ 14 | 15 | --- 16 | 17 | ## Example 18 | 19 | The following example will serve every file within any folder in the `source` directory, automatically optimizing on demand all operations, including the creation of _brotli_, _gzip_, or _deflate_. 20 | 21 | ```js 22 | import {createServer} from 'http'; 23 | import {join} from 'path'; 24 | 25 | import umeta from 'umeta'; 26 | const {dirName} = umeta(import.meta); 27 | 28 | import ucdn from 'ucdn'; 29 | const callback = cdn({ 30 | cacheTimeout: 1000 * 60, // 1 min cache 31 | source: join(dirName, 'source'), 32 | // dest: join(dirName, 'dest') 33 | }); 34 | 35 | createServer(callback).listen(8080); 36 | ``` 37 | 38 | The callback works with _Express_ too, and similar modules, where all non existent files in the source folder will be ignored, and anything else will execute regularly. 39 | 40 | ```js 41 | const {join} = require('path'); 42 | 43 | const express = require('express'); 44 | const ucdn = require('ucdn'); 45 | 46 | const app = express(); 47 | app.use(ucdn({ 48 | source: join(__dirname, 'source'), 49 | dest: join(__dirname, 'dest') 50 | })); 51 | app.get('/unknown', (req, res) => { 52 | res.writeHead(200, {'Content-Type': 'text/plain'}); 53 | res.end('OK'); 54 | }); 55 | app.listen(8080); 56 | 57 | ``` 58 | 59 | 60 | 61 | ## As binary file 62 | 63 | It is possible to bootstrap a micro CDN right away via `npx ucdn`. Pass `--help` to see options. 64 | 65 | #### The `--verbose` output 66 | 67 | If started via `--verbose` flag, each request will be logged producing the following output example: 68 | 69 | > **200** XXms /full/path.html.gzip 70 | > 71 | > **404** /full/nope.html 72 | > 73 | > **200** /favicon.ico 74 | > 75 | > **404** /full/nope.html 76 | > 77 | > **304** /full/path.html.gzip 78 | > 79 | > **500** /full/error-during-compression 80 | 81 | Please note that **200** is the file status in the *cdn*, not the response status for the browser. The status indeed indicates that the file wasn't known/compressed yet, and it took *Xms* to be generated. 82 | 83 | On the other hand, whenever a file was already known, it will be served like a **304** as the *cdn* didn't need to perform any operation. 84 | 85 | Basically, the status reflects the *cdn* and *not* whenever a browser is requesting new content or not. 86 | 87 | 88 | 89 | ### About `API` 90 | 91 | If _ucdn_ is started with an `--api ./path` flag, files in that folder will be used as fallback. 92 | 93 | The _API_ folder does not need to be reachable, or included, within static assets (_source_). 94 | 95 | ```js 96 | // @file ./api/hello.js 97 | // @start ucdn --api ./api --source ./public 98 | 99 | const headers = {'content-type': 'text/html;charset=utf-8'}; 100 | 101 | // export a function that will receive 102 | // the request and response from the server 103 | module.exports = (req, res) => { 104 | res.writeHead(200, headers); 105 | res.end('

Hello API!

'); 106 | }; 107 | ``` 108 | 109 | Please note that currently, and for the time being, files in _API_ folder must be _CommonJS_ compatible, and with a `.js` extension. 110 | 111 | If your project uses _ESM_ instead, remember to put `{"type":"commonjs"}` inside the `./api/package.json` file. 112 | 113 | 114 | 115 | ## Performance 116 | 117 | Differently from other solutions, the compression is done once, and once only, per each required static asset, reducing both _RAM_ and _CPU_ overhead in the long run, but being a bit slower than express static, with or without compressed outcome, in the very first time a file, that hasn't been optimized yet, is requested. 118 | 119 | However, once each file cache is ready, _µcdn_ is _1.2x_, up to _2.5x_, faster than express with static and compress, and it performs specially well in _IoT_ devices that are capable of running NodeJS. 120 | 121 | 122 | 123 | ### About `cacheTimeout` 124 | 125 | The purpose of this module is to do the least amount of disk operations, including lightweight operations such as `fs.stat(...)` or `fs.readFile(...)`. 126 | There are also heavy operations such the runtime compression, which should be guarded against concurrent requests. 127 | 128 | In order to do so, _µcdn_ uses an internal cache mechanism that avoid checking stats, parsing _JSON_, or compressing missing or updated assets during this timeout, which is by default _1000_ milliseconds. 129 | 130 | If you pass a timeout with value `0`, it will never check ever again anything, and all _JSON_ headers and stats results will be kept in _RAM_ until the end of the program, unless some file is missing, or some error occurs. 131 | 132 | In every other case, using a minute, up to 10 minutes, as cache timeout, is rather suggested. 133 | 134 | 135 | 136 | ## Compatibility 137 | 138 | Beside own dependencies that might have different compatibility requirements, this module works in NodeJS 10 or higher. 139 | -------------------------------------------------------------------------------- /cjs/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {mkdir, stat: fStat} = require('fs'); 3 | const {dirname} = require('path'); 4 | 5 | const idPromise = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('id-promise')); 6 | const ucompress = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('ucompress')); 7 | const umap = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('umap')); 8 | const {clear, create, json, _json} = require('ucdn-utils'); 9 | 10 | const _dir = new Map; 11 | const _pack = new Map; 12 | const _stat = new Map; 13 | 14 | const $dir = umap(_dir); 15 | const $stat = umap(_stat); 16 | 17 | const dir = (asset, timeout = 1000) => ( 18 | $dir.get(asset) || $dir.set(asset, create( 19 | timeout && setTimeout(clear, timeout, _dir, asset), 20 | (res, rej) => { 21 | mkdir(dirname(asset), {recursive: true}, err => { 22 | /* istanbul ignore if */ 23 | if (err) { 24 | clearTimeout(_dir.get(asset).timer); 25 | _dir.delete(asset); 26 | rej(err); 27 | } 28 | else 29 | res(); 30 | }); 31 | } 32 | )) 33 | ).promise; 34 | exports.dir = dir; 35 | 36 | const pack = (asset, source, target, options, timeout = 1000) => { 37 | if (_pack.has(target)) 38 | return _pack.get(target); 39 | /* istanbul ignore next */ 40 | if (_json.has(asset)) 41 | clearTimeout(_json.get(asset).timer); 42 | const promise = idPromise(`ucdn:pack:${target}`, (res, rej) => { 43 | ucompress(source, target, options).then( 44 | () => { 45 | if (timeout) 46 | setTimeout(clear, timeout, _pack, target); 47 | _json.delete(asset); 48 | json(asset, timeout).then(res, rej); 49 | }, 50 | /* istanbul ignore next */ 51 | err => { 52 | _pack.delete(target); 53 | _json.delete(asset); 54 | console.error(`\x1b[1m\x1b[31mError\x1b[0m ${source}`); 55 | console.error(err); 56 | rej(err); 57 | } 58 | ); 59 | }); 60 | _pack.set(target, promise); 61 | _json.set(asset, promise); 62 | return promise; 63 | }; 64 | exports.pack = pack; 65 | 66 | const stat = (asset, timeout = 1000) => ( 67 | $stat.get(asset) || $stat.set(asset, create( 68 | timeout && setTimeout(clear, timeout, _stat, asset), 69 | (res, rej) => { 70 | fStat(asset, (err, stats) => { 71 | if (err || !stats.isFile()) { 72 | clearTimeout(_stat.get(asset).timer); 73 | _stat.delete(asset); 74 | rej(err); 75 | } 76 | else 77 | res({ 78 | lastModified: new Date(stats.mtimeMs).toUTCString(), 79 | size: stats.size 80 | }); 81 | }); 82 | } 83 | )) 84 | ).promise; 85 | exports.stat = stat; 86 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {tmpdir} = require('os'); 3 | const {join} = require('path'); 4 | const {performance} = require('perf_hooks'); 5 | 6 | const {log} = require('essential-md'); 7 | 8 | const { 9 | compression, fallback, favicon, json, serve: justServe, serveFile, getHeaders, getPath, getURL 10 | } = require('ucdn-utils'); 11 | 12 | const {dir, pack, stat} = require('./cache.js'); 13 | 14 | const {floor} = Math; 15 | 16 | /* istanbul ignore next */ 17 | const assetName = asset => { 18 | if (/\.(br|gzip|deflate)$/.test(asset)) { 19 | const {$1} = RegExp; 20 | return '`' + asset.slice(0, -($1.length + 1)) + '`-.' + $1 + '-'; 21 | } 22 | return '`' + asset + '`'; 23 | }; 24 | 25 | /* istanbul ignore next */ 26 | const internalServerError = res => { 27 | res.writeHead(500); 28 | res.end(); 29 | }; 30 | 31 | /* istanbul ignore next */ 32 | const noPreview = (_, md, jpg) => (md === '.md' ? md : jpg); 33 | 34 | const readAndServe = (res, asset, cacheTimeout, ETag, fail, same) => { 35 | json(asset, cacheTimeout).then( 36 | headers => { 37 | serveFile(res, asset, headers, ETag, same); 38 | }, 39 | /* istanbul ignore next */ 40 | fail 41 | ); 42 | }; 43 | 44 | module.exports = ({ 45 | source, 46 | dest, 47 | headers, 48 | maxWidth, 49 | maxHeight, 50 | preview, 51 | noMinify, 52 | sourceMap, 53 | serve, 54 | cacheTimeout, 55 | verbose 56 | }) => { 57 | if (serve) 58 | return justServe(serve, cacheTimeout); 59 | const SOURCE = getPath(source); 60 | const DEST = dest ? getPath(dest) : join(tmpdir(), 'ucdn'); 61 | const options = { 62 | createFiles: true, 63 | maxWidth, maxHeight, 64 | headers, preview, noMinify, sourceMap 65 | }; 66 | return (req, res, next) => { 67 | const path = getURL(req); 68 | let real = path; 69 | if (preview) 70 | real = real.replace(/(\.md)?\.preview(\.(?:jpe?g|html))$/i, noPreview); 71 | if (sourceMap) 72 | real = real.replace(/(\.m?js)\.(?:source\1|map)$/, '$1'); 73 | const original = SOURCE + real.replace(/\/u_modules\//g, '/node_modules/'); 74 | stat(original, cacheTimeout).then( 75 | ({lastModified, size}) => { 76 | if (path === '/favicon.ico') { 77 | /* istanbul ignore if */ 78 | if (verbose) 79 | log(` *200* -favicon- ${original}`); 80 | favicon(res, original, size, headers); 81 | } 82 | else { 83 | const {AcceptEncoding, ETag, Since} = getHeaders(req); 84 | const asset = DEST + compression(path, AcceptEncoding); 85 | const create = (time) => { 86 | const target = DEST + real; 87 | const waitForIt = target + '.wait'; 88 | /* istanbul ignore next */ 89 | const fail = () => { 90 | if (verbose) 91 | log(` *500* ${assetName(asset)}`); 92 | internalServerError(res); 93 | }; 94 | dir(waitForIt, cacheTimeout).then( 95 | () => { 96 | pack(asset, original, target, options, cacheTimeout).then( 97 | () => { 98 | /* istanbul ignore if */ 99 | if (verbose) { 100 | if (time) 101 | time = floor(performance.now() - time); 102 | log(` *200* ${time ? `-${time}ms- ` : ''}${assetName(asset)}`); 103 | } 104 | readAndServe(res, asset, cacheTimeout, ETag, fail, false); 105 | }, 106 | /* istanbul ignore next */ 107 | fail 108 | ); 109 | }, 110 | /* istanbul ignore next */ 111 | fail 112 | ); 113 | }; 114 | json(asset, cacheTimeout).then( 115 | headers => { 116 | /* istanbul ignore else */ 117 | if (lastModified === headers['Last-Modified']) { 118 | /* istanbul ignore if */ 119 | if (verbose) 120 | log(` *304* ${assetName(asset)}`); 121 | serveFile(res, asset, headers, ETag, lastModified === Since); 122 | } 123 | else 124 | create(0); 125 | }, 126 | () => { 127 | /* istanbul ignore next */ 128 | create(verbose ? performance.now() : 0); 129 | } 130 | ); 131 | } 132 | }, 133 | fallback(req, res, next) 134 | ); 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /esm/cache.js: -------------------------------------------------------------------------------- 1 | import {mkdir, stat as fStat} from 'fs'; 2 | import {dirname} from 'path'; 3 | 4 | import idPromise from 'id-promise'; 5 | import ucompress from 'ucompress'; 6 | import umap from 'umap'; 7 | import {clear, create, json, _json} from 'ucdn-utils'; 8 | 9 | const _dir = new Map; 10 | const _pack = new Map; 11 | const _stat = new Map; 12 | 13 | const $dir = umap(_dir); 14 | const $stat = umap(_stat); 15 | 16 | export const dir = (asset, timeout = 1000) => ( 17 | $dir.get(asset) || $dir.set(asset, create( 18 | timeout && setTimeout(clear, timeout, _dir, asset), 19 | (res, rej) => { 20 | mkdir(dirname(asset), {recursive: true}, err => { 21 | /* istanbul ignore if */ 22 | if (err) { 23 | clearTimeout(_dir.get(asset).timer); 24 | _dir.delete(asset); 25 | rej(err); 26 | } 27 | else 28 | res(); 29 | }); 30 | } 31 | )) 32 | ).promise; 33 | 34 | export const pack = (asset, source, target, options, timeout = 1000) => { 35 | if (_pack.has(target)) 36 | return _pack.get(target); 37 | /* istanbul ignore next */ 38 | if (_json.has(asset)) 39 | clearTimeout(_json.get(asset).timer); 40 | const promise = idPromise(`ucdn:pack:${target}`, (res, rej) => { 41 | ucompress(source, target, options).then( 42 | () => { 43 | if (timeout) 44 | setTimeout(clear, timeout, _pack, target); 45 | _json.delete(asset); 46 | json(asset, timeout).then(res, rej); 47 | }, 48 | /* istanbul ignore next */ 49 | err => { 50 | _pack.delete(target); 51 | _json.delete(asset); 52 | console.error(`\x1b[1m\x1b[31mError\x1b[0m ${source}`); 53 | console.error(err); 54 | rej(err); 55 | } 56 | ); 57 | }); 58 | _pack.set(target, promise); 59 | _json.set(asset, promise); 60 | return promise; 61 | }; 62 | 63 | export const stat = (asset, timeout = 1000) => ( 64 | $stat.get(asset) || $stat.set(asset, create( 65 | timeout && setTimeout(clear, timeout, _stat, asset), 66 | (res, rej) => { 67 | fStat(asset, (err, stats) => { 68 | if (err || !stats.isFile()) { 69 | clearTimeout(_stat.get(asset).timer); 70 | _stat.delete(asset); 71 | rej(err); 72 | } 73 | else 74 | res({ 75 | lastModified: new Date(stats.mtimeMs).toUTCString(), 76 | size: stats.size 77 | }); 78 | }); 79 | } 80 | )) 81 | ).promise; 82 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import {tmpdir} from 'os'; 2 | import {join} from 'path'; 3 | import {performance} from 'perf_hooks'; 4 | 5 | import {log} from 'essential-md'; 6 | 7 | import { 8 | compression, fallback, favicon, json, 9 | serve as justServe, serveFile, 10 | getHeaders, getPath, getURL 11 | } from 'ucdn-utils'; 12 | 13 | import {dir, pack, stat} from './cache.js'; 14 | 15 | const {floor} = Math; 16 | 17 | /* istanbul ignore next */ 18 | const assetName = asset => { 19 | if (/\.(br|gzip|deflate)$/.test(asset)) { 20 | const {$1} = RegExp; 21 | return '`' + asset.slice(0, -($1.length + 1)) + '`-.' + $1 + '-'; 22 | } 23 | return '`' + asset + '`'; 24 | }; 25 | 26 | /* istanbul ignore next */ 27 | const internalServerError = res => { 28 | res.writeHead(500); 29 | res.end(); 30 | }; 31 | 32 | /* istanbul ignore next */ 33 | const noPreview = (_, md, jpg) => (md === '.md' ? md : jpg); 34 | 35 | const readAndServe = (res, asset, cacheTimeout, ETag, fail, same) => { 36 | json(asset, cacheTimeout).then( 37 | headers => { 38 | serveFile(res, asset, headers, ETag, same); 39 | }, 40 | /* istanbul ignore next */ 41 | fail 42 | ); 43 | }; 44 | 45 | export default ({ 46 | source, 47 | dest, 48 | headers, 49 | maxWidth, 50 | maxHeight, 51 | preview, 52 | noMinify, 53 | sourceMap, 54 | serve, 55 | cacheTimeout, 56 | verbose 57 | }) => { 58 | if (serve) 59 | return justServe(serve, cacheTimeout); 60 | const SOURCE = getPath(source); 61 | const DEST = dest ? getPath(dest) : join(tmpdir(), 'ucdn'); 62 | const options = { 63 | createFiles: true, 64 | maxWidth, maxHeight, 65 | headers, preview, noMinify, sourceMap 66 | }; 67 | return (req, res, next) => { 68 | const path = getURL(req); 69 | let real = path; 70 | if (preview) 71 | real = real.replace(/(\.md)?\.preview(\.(?:jpe?g|html))$/i, noPreview); 72 | if (sourceMap) 73 | real = real.replace(/(\.m?js)\.(?:source\1|map)$/, '$1'); 74 | const original = SOURCE + real.replace(/\/u_modules\//g, '/node_modules/'); 75 | stat(original, cacheTimeout).then( 76 | ({lastModified, size}) => { 77 | if (path === '/favicon.ico') { 78 | /* istanbul ignore if */ 79 | if (verbose) 80 | log(` *200* -favicon- ${original}`); 81 | favicon(res, original, size, headers); 82 | } 83 | else { 84 | const {AcceptEncoding, ETag, Since} = getHeaders(req); 85 | const asset = DEST + compression(path, AcceptEncoding); 86 | const create = (time) => { 87 | const target = DEST + real; 88 | const waitForIt = target + '.wait'; 89 | /* istanbul ignore next */ 90 | const fail = () => { 91 | if (verbose) 92 | log(` *500* ${assetName(asset)}`); 93 | internalServerError(res); 94 | }; 95 | dir(waitForIt, cacheTimeout).then( 96 | () => { 97 | pack(asset, original, target, options, cacheTimeout).then( 98 | () => { 99 | /* istanbul ignore if */ 100 | if (verbose) { 101 | if (time) 102 | time = floor(performance.now() - time); 103 | log(` *200* ${time ? `-${time}ms- ` : ''}${assetName(asset)}`); 104 | } 105 | readAndServe(res, asset, cacheTimeout, ETag, fail, false); 106 | }, 107 | /* istanbul ignore next */ 108 | fail 109 | ); 110 | }, 111 | /* istanbul ignore next */ 112 | fail 113 | ); 114 | }; 115 | json(asset, cacheTimeout).then( 116 | headers => { 117 | /* istanbul ignore else */ 118 | if (lastModified === headers['Last-Modified']) { 119 | /* istanbul ignore if */ 120 | if (verbose) 121 | log(` *304* ${assetName(asset)}`); 122 | serveFile(res, asset, headers, ETag, lastModified === Since); 123 | } 124 | else 125 | create(0); 126 | }, 127 | () => { 128 | /* istanbul ignore next */ 129 | create(verbose ? performance.now() : 0); 130 | } 131 | ); 132 | } 133 | }, 134 | fallback(req, res, next) 135 | ); 136 | }; 137 | }; 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucdn", 3 | "version": "0.22.0", 4 | "description": "A µcompress based CDN utility, compatible with both Express and native http module", 5 | "bin": "./server.cjs", 6 | "main": "./cjs/index.js", 7 | "scripts": { 8 | "build": "npm run cjs && npm run test", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 11 | "test": "rm -rf test/dest && rm -rf /tmp/ucdn && nyc node test/index.js && rm test/index.test" 12 | }, 13 | "keywords": [ 14 | "ucompress", 15 | "CDN", 16 | "Express", 17 | "http" 18 | ], 19 | "author": "Andrea Giammarchi", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "ascjs": "^5.0.1", 23 | "coveralls": "^3.1.0", 24 | "nyc": "^15.1.0" 25 | }, 26 | "module": "./esm/index.js", 27 | "type": "module", 28 | "exports": { 29 | "import": "./esm/index.js", 30 | "default": "./cjs/index.js" 31 | }, 32 | "dependencies": { 33 | "essential-md": "^0.3.1", 34 | "id-promise": "^0.3.0", 35 | "ucdn-utils": "^0.5.2", 36 | "ucompress": "^0.22.1" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/WebReflection/ucdn.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/WebReflection/ucdn/issues" 44 | }, 45 | "homepage": "https://github.com/WebReflection/ucdn#readme" 46 | } 47 | -------------------------------------------------------------------------------- /server.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cluster = require('cluster'); 4 | const {readdir, exists, rm, rmdir} = require('fs'); 5 | const {createServer} = require('http'); 6 | const {cpus, networkInterfaces} = require('os'); 7 | const {join, resolve} = require('path'); 8 | 9 | const {error, log, warn} = require('essential-md'); 10 | 11 | const cdn = require('./cjs/index.js'); 12 | 13 | const {fork, isMaster} = cluster; 14 | 15 | const cwd = process.cwd(); 16 | 17 | const apiCache = new Map; 18 | 19 | let port = 8080; 20 | let cacheTimeout = 300000; 21 | let clusters = 0; 22 | let api = ''; 23 | let serve = '.'; 24 | let source = '.'; 25 | let dest = ''; 26 | let debug = false; 27 | let help = false; 28 | let preview = false; 29 | let isServing = false; 30 | let notServing = false; 31 | let noImport = false; 32 | let noMinify = false; 33 | let sourceMap = false; 34 | let verbose = false; 35 | let maxWidth, maxHeight; 36 | 37 | for (let 38 | {max, min} = Math, {argv} = process, 39 | {length} = argv, i = 2; i < length; i++ 40 | ) { 41 | 42 | // utils 43 | const asInt = ({$1}) => parseInt($1 ? $1.slice(1) : argv[++i], 10); 44 | const asString = ({$1}) => ($1 ? $1.slice(1) : argv[++i]); 45 | 46 | switch (true) { 47 | 48 | // integers 49 | case /^-t$/.test(argv[i]): 50 | case /^--cache-timeout(=\d+)?$/.test(argv[i]): 51 | cacheTimeout = asInt(RegExp); 52 | break; 53 | case /^--(?:cluster|fork)s?(=\d+)?$/.test(argv[i]): 54 | const amount = asInt(RegExp); 55 | clusters = max(0, min(amount, cpus().length)) || 0; 56 | break; 57 | case /^--max-width(=\d+)?$/.test(argv[i]): 58 | maxWidth = asInt(RegExp); 59 | break; 60 | case /^--max-height(=\d+)?$/.test(argv[i]): 61 | maxHeight = asInt(RegExp); 62 | break; 63 | case /^-p$/.test(argv[i]): 64 | case /^--port(=\d+)?$/.test(argv[i]): 65 | port = asInt(RegExp); 66 | break; 67 | 68 | // strings as paths 69 | case /^--api(=.+)?$/.test(argv[i]): 70 | api = asString(RegExp); 71 | break; 72 | case /^-d$/.test(argv[i]): 73 | case /^--(?:dest|cache)(=.+)?$/.test(argv[i]): 74 | dest = asString(RegExp); 75 | notServing = true; 76 | break; 77 | case /^--serve(=.+)?$/.test(argv[i]): 78 | serve = asString(RegExp); 79 | isServing = true; 80 | break; 81 | case /^-s$/.test(argv[i]): 82 | case /^--source(=.+)?$/.test(argv[i]): 83 | source = asString(RegExp); 84 | notServing = true; 85 | break; 86 | 87 | // no value needed 88 | case /^--debug$/.test(argv[i]): 89 | debug = true; 90 | verbose = true; 91 | noMinify = true; 92 | cacheTimeout = 500; 93 | break; 94 | case /^--no-imports?$/.test(argv[i]): 95 | noImport = true; 96 | break; 97 | case /^--no-min$/.test(argv[i]): 98 | case /^--no-minify$/.test(argv[i]): 99 | noMinify = true; 100 | break; 101 | case /^--preview$/.test(argv[i]): 102 | case /^--with-preview$/.test(argv[i]): 103 | preview = true; 104 | break; 105 | case /^--source-map$/.test(argv[i]): 106 | case /^--with-source-map$/.test(argv[i]): 107 | sourceMap = true; 108 | break; 109 | case /^-v$/.test(argv[i]): 110 | case /^--verbose$/.test(argv[i]): 111 | verbose = true; 112 | break; 113 | case /^--help$/.test(argv[i]): 114 | default: 115 | help = true; 116 | i = length; 117 | break; 118 | } 119 | } 120 | 121 | if (debug) 122 | (rm || rmdir)(dest ? resolve(cwd, dest) : '/tmp/ucdn', {recursive: true, force: true}, Object); 123 | 124 | const header = `# micro cdn v${require(join(__dirname, 'package.json')).version}`; 125 | if (help || (notServing && isServing)) { 126 | log(` 127 | ${header} 128 | -https://github.com/WebReflection/ucdn- 129 | 130 | *ucdn --source ./path/* 131 | \`--source ./\` -# (\`-s\`) path to serve as CDN, default current folder- 132 | \`--dest /tmp\` -# (\`-d\`) CDN cache path, default /tmp/ucdn- 133 | \`--cache-timeout X\` -# (\`-t\`) cache expiration in ms, default 300000- 134 | \`--port XXXX\` -# (\`-p\`) port to use, default 0 (any available port)- 135 | \`--cluster(s) X\` -# number of forks, default 0- 136 | \`--serve /path\` -# serve a CDN ready path without any runtime- 137 | \`--verbose\` -# logs operations- 138 | \`--debug\` -# 500ms cache timeout + no minification + verbose- 139 | \`--api ./api\` -# use files in folder as API fallback- 140 | 141 | *ucompress* options 142 | \`--max-width X\` -# max images width in pixels- 143 | \`--max-height X\` -# max images height in pixels- 144 | \`--with-preview\` -# enables *.preview.jpeg images- 145 | \`--with-source-map\` -# enables source maps- 146 | \`--no-import\` -# avoid resolving imports- 147 | \`--no-minify\` -# do not minify sources- 148 | 149 | *aliases* 150 | \`--fork(s) X\` -# alias for \`--cluster(s)\`- 151 | \`--cache /tmp\` -# alias for \`--dest\`- 152 | `); 153 | } 154 | else if (isMaster && 0 < clusters) { 155 | let forks = clusters; 156 | let toBeGreeted = true; 157 | const onGreetings = usedPort => { 158 | if (toBeGreeted) { 159 | toBeGreeted = !toBeGreeted; 160 | greetings(usedPort); 161 | } 162 | }; 163 | while (forks--) 164 | fork().on('message', onGreetings); 165 | cluster.on('exit', ({process}, code, signal) => { 166 | warn(`Worker *${process.pid}* died with code *${code}* and signal *${signal}*`); 167 | if (!(code == 1 && !signal)) 168 | fork(); 169 | }); 170 | } 171 | else { 172 | const hasAPI = api !== ''; 173 | if (hasAPI) 174 | api = resolve(cwd, api); 175 | const base = resolve(cwd, source); 176 | const meta = ''; 177 | const style = ''; 178 | const handler = cdn(isServing ? {cacheTimeout, serve} : { 179 | dest: dest ? resolve(cwd, dest) : '', 180 | source: base, 181 | cacheTimeout, 182 | maxWidth, 183 | maxHeight, 184 | preview, 185 | sourceMap, 186 | noImport, 187 | noMinify, 188 | verbose 189 | }); 190 | const checkAPI = (req, res, url) => { 191 | const js = join(api, `${url.slice(1)}.js`); 192 | exists(js, exists => { 193 | if (exists) { 194 | try { 195 | const module = require(js); 196 | module(req, res); 197 | apiCache.set(url, module); 198 | } 199 | catch (o_O) { 200 | console.error(o_O); 201 | fail(res, url); 202 | } 203 | } 204 | else 205 | fail(res, url); 206 | }); 207 | }; 208 | const fail = (res, url) => { 209 | if (verbose) 210 | log(` *404* \`${url}\``); 211 | res.writeHead(404); 212 | res.end(); 213 | }; 214 | const redirect = (res, url) => { 215 | if (verbose) 216 | log(` *302* \`${url}/\``); 217 | res.writeHead(302, {'Location': `${url}/`}); 218 | res.end(); 219 | }; 220 | const next = isServing ? 221 | ((req, res) => { 222 | const url = req.url.replace(/\/$/, ''); 223 | if (hasAPI && apiCache.has(url)) 224 | apiCache.get(url)(req, res); 225 | else if (/\.\w+(?:\?.*)?$/.test(url)) 226 | fail(res, url); 227 | else { 228 | exists(join(base, url.slice(1), 'index.html'), exists => { 229 | if (exists) 230 | redirect(res, url); 231 | else if (hasAPI) 232 | checkAPI(req, res, url); 233 | else 234 | fail(res, url); 235 | }); 236 | } 237 | }) : 238 | ((req, res) => { 239 | const url = req.url.replace(/\/$/, ''); 240 | if (hasAPI && apiCache.has(url)) 241 | apiCache.get(url)(req, res); 242 | else if (/\.\w+(?:\?.*)?$/.test(url)) 243 | fail(res, url); 244 | else { 245 | exists(join(base, url.slice(1), 'index.html'), exists => { 246 | if (exists) 247 | redirect(res, url); 248 | else { 249 | const dir = join(base, url.slice(1)); 250 | readdir(dir, (err, files) => { 251 | if (err) { 252 | if (hasAPI) 253 | checkAPI(req, res, url); 254 | else 255 | fail(res, url); 256 | } 257 | else { 258 | if (verbose) 259 | log(` *200* -listing- \`${dir}\``); 260 | res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'}); 261 | res.write(`${ 262 | meta 263 | }${ 264 | dir 265 | }${ 266 | style 267 | }

${ 268 | dir 269 | }

`); 279 | res.end(); 280 | } 281 | }); 282 | } 283 | }); 284 | } 285 | }) 286 | ; 287 | createServer(($, _) => { handler($, _, next); }) 288 | .on('error', function() { 289 | if (port == 8080) 290 | this.listen(0); 291 | else { 292 | error(` port *${port}* is unavailable`); 293 | process.exit(1); 294 | } 295 | }) 296 | .listen(port, greetings); 297 | } 298 | 299 | function greetings(newPort = this.address().port) { 300 | if (isMaster) { 301 | const checks = `(checked each ${(cacheTimeout / 60000) >>> 0} min)`; 302 | log(`\n${header}`); 303 | if (newPort != port) 304 | warn(` port *${port}* not available, using *${newPort}* instead`); 305 | if (isServing) { 306 | log(` -serving:- \`${resolve(cwd, serve)}\` -${checks}-`); 307 | } 308 | else { 309 | log(` -source:- \`${resolve(cwd, source)}\` -${checks}-`); 310 | log(` -cache:- \x1b[2m\`${dest ? resolve(cwd, dest) : '/tmp/ucdn'}\`\x1b[0m`); 311 | } 312 | if (api !== '') 313 | log(` -api:- \x1b[2m\`${api}\`\x1b[0m`); 314 | let config = []; 315 | if (clusters) 316 | config.push(`${clusters} -forks-`); 317 | if (preview) 318 | config.push('-preview-'); 319 | if (sourceMap) 320 | config.push('-source map-'); 321 | if (noImport) 322 | config.push('-no import-'); 323 | if (noMinify) 324 | config.push('-no minification-'); 325 | if (maxWidth) 326 | config.push(`-w${maxWidth}px-`); 327 | if (maxHeight) 328 | config.push(`-h${maxHeight}px-`); 329 | if (verbose) 330 | config.push(`-verbose-`); 331 | if (config.length) 332 | log(` -config:- ${config.join('-,- ')}`); 333 | log(` -visit:- *http://localhost${newPort == 80 ? '' : `:${newPort}`}/*`); 334 | const interfaces = networkInterfaces(); 335 | Object.keys(interfaces).forEach(key => { 336 | interfaces[key].forEach(iFace => { 337 | const {address, family} = iFace; 338 | if (family === 'IPv4' && address !== '127.0.0.1') 339 | log(` *http://${address}${newPort == 80 ? '' : `:${newPort}`}/*`); 340 | }); 341 | }); 342 | log(''); 343 | } 344 | else 345 | process.send(newPort); 346 | } 347 | -------------------------------------------------------------------------------- /test/api/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res) => { 2 | res.writeHead(200, {'content-type': 'text/html;charset=utf-8'}); 3 | res.end('

Hello API!

'); 4 | }; 5 | -------------------------------------------------------------------------------- /test/api/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} 2 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | Hello 👋 10 | 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {createWriteStream, statSync} = require('fs'); 2 | const {join} = require('path'); 3 | 4 | const createResponse = callback => { 5 | const response = createWriteStream(join(__dirname, 'index.test')); 6 | const operations = []; 7 | response.writeHead = (...args) => { 8 | operations.push(args); 9 | }; 10 | response.end = (...args) => { 11 | operations.push(args); 12 | callback(operations); 13 | }; 14 | return response; 15 | }; 16 | 17 | const createRequest = url => ({ 18 | url, 19 | headers: { 20 | get acceptEncoding() { 21 | return this['accept-encoding'] || ''; 22 | }, 23 | set acceptEncoding(encoding) { 24 | this['accept-encoding'] = encoding; 25 | }, 26 | get ifNoneMatch() { 27 | return this['if-none-match'] || ''; 28 | }, 29 | set ifNoneMatch(ETag) { 30 | this['if-none-match'] = ETag; 31 | }, 32 | get ifModifiedSince() { 33 | return this['if-modified-since'] || ''; 34 | }, 35 | set ifModifiedSince(value) { 36 | this['if-modified-since'] = new Date(value).toUTCString(); 37 | } 38 | } 39 | }); 40 | 41 | const cdn = require('../cjs'); 42 | 43 | let requestHandler = cdn({ 44 | source: join(__dirname, 'source'), 45 | dest: join(__dirname, 'dest'), 46 | headers: { 47 | 'X-Powered-By': 'µcdn' 48 | } 49 | }); 50 | 51 | Promise.resolve('\x1b[1mµcdn\x1b[0m') 52 | .then(name => new Promise(resolve => { 53 | console.log(name); 54 | const path = '/favicon.ico?whatever'; 55 | requestHandler( 56 | createRequest(path), 57 | createResponse(operations => { 58 | console.assert(operations.length === 2, 'correct amount of operations'); 59 | const [code, headers] = operations.shift(); 60 | const content = operations.shift(); 61 | console.assert(content.length < 1, 'correct content'); 62 | console.assert(code === 200, 'correct code'); 63 | console.assert(headers['Content-Length'] === 16201, 'correct length'); 64 | console.assert(headers['Content-Type'] === 'image/vnd.microsoft.icon', 'correct mime'); 65 | resolve(path); 66 | }) 67 | ); 68 | })) 69 | .then(name => new Promise(resolve => { 70 | console.log(name); 71 | const path = '/unknown.file'; 72 | const request = createRequest(path); 73 | request.headers.acceptEncoding = 'gzip'; 74 | requestHandler( 75 | request, 76 | createResponse(operations => { 77 | console.assert(operations.length === 2, 'correct amount of operations'); 78 | const [code, headers] = operations.shift(); 79 | const content = operations.shift(); 80 | console.assert(content.length < 1, 'correct content'); 81 | console.assert(code === 404, 'correct code'); 82 | console.assert(!headers, 'correct headers'); 83 | resolve(path); 84 | }) 85 | ); 86 | })) 87 | .then(name => new Promise(resolve => { 88 | console.log(name); 89 | const path = '/unknown.next'; 90 | const request = createRequest(path); 91 | request.headers.acceptEncoding = 'gzip'; 92 | requestHandler( 93 | request, 94 | createResponse(operations => { 95 | console.assert(operations.length === 2, 'correct amount of operations'); 96 | const [code, headers] = operations.shift(); 97 | const content = operations.shift(); 98 | console.assert(content.length < 1, 'correct content'); 99 | console.assert(code === 404, 'correct code'); 100 | console.assert(!headers, 'correct headers'); 101 | }), 102 | () => resolve(path) 103 | ); 104 | })) 105 | .then(name => new Promise(resolve => { 106 | console.log(name); 107 | const path = '/text.txt'; 108 | const request = createRequest(path); 109 | request.headers.acceptEncoding = 'gzip'; 110 | requestHandler( 111 | request, 112 | createResponse(operations => { 113 | console.assert(operations.length === 2, 'correct amount of operations'); 114 | const [code, headers] = operations.shift(); 115 | const content = operations.shift(); 116 | console.assert(content.length < 1, 'correct content'); 117 | console.assert(code === 200, 'correct code'); 118 | console.assert(headers['Content-Length'] === 1363, 'correct length'); 119 | console.assert(headers['Content-Type'] === 'text/plain; charset=UTF-8', 'correct mime'); 120 | console.assert(headers['ETag'] === '"553-Pqern58SsN5hVxit"', 'correct ETag'); 121 | console.assert(headers['X-Powered-By'] === 'µcdn', 'correct headers'); 122 | resolve(path); 123 | }) 124 | ); 125 | })) 126 | .then(name => new Promise(resolve => { 127 | console.log(name); 128 | const path = '/text.txt?again'; 129 | const request = createRequest(path); 130 | request.headers.acceptEncoding = 'gzip'; 131 | request.headers.ifNoneMatch = '"553-Pqern58SsN5hVxit"'; 132 | request.headers.ifModifiedSince = statSync(join(__dirname, 'source', 'text.txt')).mtimeMs; 133 | requestHandler( 134 | request, 135 | createResponse(operations => { 136 | console.assert(operations.length === 2, 'correct amount of operations'); 137 | const [code, headers] = operations.shift(); 138 | const content = operations.shift(); 139 | console.assert(content.length < 1, 'correct content'); 140 | console.assert(code === 304, 'correct code'); 141 | console.assert(headers['Content-Length'] === 1363, 'correct length'); 142 | console.assert(headers['Content-Type'] === 'text/plain; charset=UTF-8', 'correct mime'); 143 | console.assert(headers['ETag'] === '"553-Pqern58SsN5hVxit"', 'correct ETag'); 144 | console.assert(headers['X-Powered-By'] === 'µcdn', 'correct headers'); 145 | resolve(path); 146 | }) 147 | ); 148 | })) 149 | .then(name => { 150 | requestHandler = cdn({ 151 | maxWidth: 320, 152 | cacheTimeout: 100, 153 | source: './test/source', 154 | preview: true, 155 | sourceMap: true 156 | }); 157 | return name; 158 | }) 159 | .then(name => new Promise(resolve => { 160 | console.log(name); 161 | const path = '/text.txt?one-more-time'; 162 | const request = createRequest(path); 163 | request.headers.acceptEncoding = 'gzip'; 164 | request.headers.ifNoneMatch = '"553-Pqern58SsN5hVxit"'; 165 | request.headers.ifModifiedSince = (new Date).toISOString(); 166 | requestHandler( 167 | request, 168 | createResponse(operations => { 169 | console.assert(operations.length === 2, 'correct amount of operations'); 170 | const [code, headers] = operations.shift(); 171 | const content = operations.shift(); 172 | console.assert(content.length < 1, 'correct content'); 173 | console.assert(code === 200, 'correct code'); 174 | console.assert(headers['Content-Length'] === 1363, 'correct length'); 175 | console.assert(headers['Content-Type'] === 'text/plain; charset=UTF-8', 'correct mime'); 176 | console.assert(headers['ETag'] === '"553-Pqern58SsN5hVxit"', 'correct ETag'); 177 | console.assert(!headers['X-Powered-By'], 'correct headers'); 178 | resolve(path); 179 | }) 180 | ); 181 | })) 182 | .then(name => new Promise(resolve => { 183 | console.log(name); 184 | const path = '/text.txt?last-time-maybe'; 185 | const request = createRequest(path); 186 | request.headers.acceptEncoding = ''; 187 | requestHandler( 188 | request, 189 | createResponse(operations => { 190 | console.assert(operations.length === 2, 'correct amount of operations'); 191 | const [code, headers] = operations.shift(); 192 | const content = operations.shift(); 193 | console.assert(content.length < 1, 'correct content'); 194 | console.assert(code === 200, 'correct code'); 195 | console.assert(headers['Content-Length'] === 3356, 'correct length'); 196 | console.assert(headers['Content-Type'] === 'text/plain; charset=UTF-8', 'correct mime'); 197 | console.assert(headers['ETag'] === '"d1c-BnkCkKBJ6IhARixM"', 'correct ETag'); 198 | resolve(path); 199 | }) 200 | ); 201 | })) 202 | .then(name => new Promise(resolve => { 203 | console.log(name); 204 | const path = '/archibold.preview.jpg'; 205 | const request = createRequest(path); 206 | request.headers.acceptEncoding = ''; 207 | requestHandler( 208 | request, 209 | createResponse(operations => { 210 | console.assert(operations.length === 2, 'correct amount of operations'); 211 | const [code, headers] = operations.shift(); 212 | const content = operations.shift(); 213 | console.assert(content.length < 1, 'correct content'); 214 | console.assert(code === 200, 'correct code'); 215 | console.assert(headers['Content-Length'] === 686, 'correct length'); 216 | console.assert(headers['Content-Type'] === 'image/jpeg', 'correct mime'); 217 | console.assert(headers['ETag'] === '"2ae-e5yCsJisKX7bkYDy"', 'correct ETag'); 218 | resolve(path); 219 | }) 220 | ); 221 | })) 222 | .then(name => new Promise(resolve => { 223 | console.log(name); 224 | const path = '/archibold.jpg'; 225 | const request = createRequest(path); 226 | request.headers.acceptEncoding = ''; 227 | requestHandler( 228 | request, 229 | createResponse(operations => { 230 | console.assert(operations.length === 2, 'correct amount of operations'); 231 | const [code, headers] = operations.shift(); 232 | const content = operations.shift(); 233 | console.assert(content.length < 1, 'correct content'); 234 | console.assert(code === 200, 'correct code'); 235 | console.assert(headers['Content-Length'] === 4816, 'correct length'); 236 | console.assert(headers['Content-Type'] === 'image/jpeg', 'correct mime'); 237 | console.assert(headers['ETag'] === '"12d0-2VQbuqh6MestOXlN"', 'correct ETag'); 238 | resolve(path); 239 | }) 240 | ); 241 | })) 242 | .then(name => { 243 | requestHandler = cdn({ 244 | maxHeight: 320, 245 | cacheTimeout: 0, 246 | source: './test/source' 247 | }); 248 | return name; 249 | }) 250 | .then(name => new Promise(resolve => { 251 | console.log(name); 252 | const path = '/package.json'; 253 | const request = createRequest(path); 254 | request.headers.acceptEncoding = 'br'; 255 | requestHandler( 256 | request, 257 | createResponse(operations => { 258 | console.assert(operations.length === 2, 'correct amount of operations'); 259 | const [code, headers] = operations.shift(); 260 | const content = operations.shift(); 261 | console.assert(content.length < 1, 'correct content'); 262 | console.assert(code === 200, 'correct code'); 263 | console.assert(headers['Content-Length'] === 454, 'correct length'); 264 | console.assert(headers['Content-Type'] === 'application/json; charset=UTF-8', 'correct mime'); 265 | console.assert(headers['ETag'] === '"1c6-c5WYF55FEIj0B3QU"', 'correct ETag'); 266 | resolve(path); 267 | }) 268 | ); 269 | })) 270 | .then(name => new Promise(resolve => { 271 | console.log(name); 272 | const path = '/index.html'; 273 | const request = createRequest(path); 274 | request.headers.acceptEncoding = 'br'; 275 | requestHandler( 276 | request, 277 | createResponse(operations => { 278 | console.assert(operations.length === 2, 'correct amount of operations'); 279 | const [code, headers] = operations.shift(); 280 | const content = operations.shift(); 281 | console.assert(content.length < 1, 'correct content'); 282 | console.assert(code === 200, 'correct code'); 283 | console.assert(headers['Content-Length'] === 84, 'correct length'); 284 | console.assert(headers['Content-Type'] === 'text/html; charset=UTF-8', 'correct mime'); 285 | console.assert(headers['ETag'] === '"54-jtoG/c9bRSWQB+gy"', 'correct ETag'); 286 | resolve(path); 287 | }) 288 | ); 289 | })) 290 | .then(name => new Promise(resolve => { 291 | console.log(name); 292 | const path = '/benja-dark.svg'; 293 | const request = createRequest(path); 294 | request.headers.acceptEncoding = ''; 295 | let i = 0; 296 | const done = () => { 297 | if (++i === 2) 298 | resolve(path); 299 | }; 300 | requestHandler( 301 | request, 302 | createResponse(operations => { 303 | console.assert(operations.length === 2, 'correct amount of operations'); 304 | const [code, headers] = operations.shift(); 305 | const content = operations.shift(); 306 | console.assert(content.length < 1, 'correct content'); 307 | console.assert(code === 200, 'correct code'); 308 | console.assert(headers['Content-Length'] === 3405, 'correct length'); 309 | console.assert(headers['Content-Type'] === 'image/svg+xml', 'correct mime'); 310 | console.assert(headers['ETag'] === '"d4d-5Qa/5/7tPdtEx1VK"', 'correct ETag'); 311 | done(); 312 | }) 313 | ); 314 | requestHandler( 315 | request, 316 | createResponse(operations => { 317 | console.assert(operations.length === 2, 'correct amount of operations'); 318 | const [code, headers] = operations.shift(); 319 | const content = operations.shift(); 320 | console.assert(content.length < 1, 'correct content'); 321 | console.assert(code === 200, 'correct code'); 322 | console.assert(headers['Content-Length'] === 3405, 'correct length'); 323 | console.assert(headers['Content-Type'] === 'image/svg+xml', 'correct mime'); 324 | console.assert(headers['ETag'] === '"d4d-5Qa/5/7tPdtEx1VK"', 'correct ETag'); 325 | done(); 326 | }) 327 | ); 328 | })) 329 | .then(name => new Promise(resolve => { 330 | console.log(name); 331 | requestHandler = cdn({serve: './test/dest'}); 332 | const path = '/text.txt'; 333 | const request = createRequest(path); 334 | request.headers.acceptEncoding = 'deflate'; 335 | request.headers.ifNoneMatch = '"547-MRRsLNkHQ2IWwyfp"'; 336 | requestHandler( 337 | request, 338 | createResponse(operations => { 339 | console.assert(operations.length === 2, 'correct amount of operations'); 340 | const [code, headers] = operations.shift(); 341 | const content = operations.shift(); 342 | console.assert(content.length < 1, 'correct content'); 343 | console.assert(code === 304, 'correct code'); 344 | console.assert(headers['Content-Length'] === 1351, 'correct length'); 345 | console.assert(headers['Content-Type'] === 'text/plain; charset=UTF-8', 'correct mime'); 346 | console.assert(headers['ETag'] === '"547-MRRsLNkHQ2IWwyfp"', 'correct ETag'); 347 | console.assert(headers['X-Powered-By'] === 'µcdn', 'correct headers'); 348 | resolve(path); 349 | }) 350 | ); 351 | })) 352 | .then(console.log) 353 | ; 354 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/server.cjs: -------------------------------------------------------------------------------- 1 | const {fork, isMaster} = require('cluster'); 2 | const {createServer} = require('http'); 3 | const {cpus} = require('os'); 4 | const {join} = require('path'); 5 | 6 | const cdn = require('../cjs'); 7 | 8 | if (isMaster) 9 | cpus().forEach(() => fork()); 10 | else { 11 | const callback = cdn({ 12 | cacheTimeout: 10000, 13 | source: join(__dirname, 'source'), 14 | dest: join(__dirname, 'dest') 15 | }); 16 | createServer(callback).listen(8080); 17 | } 18 | -------------------------------------------------------------------------------- /test/source/archibold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/archibold.jpg -------------------------------------------------------------------------------- /test/source/benja-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 24 | 28 | 32 | 36 | 40 | 44 | 45 | 54 | 56 | 60 | 64 | 65 | 74 | 75 | 77 | 78 | 80 | image/svg+xml 81 | 83 | 84 | 85 | 86 | 87 | 90 | 98 | 106 | 110 | 111 | 115 | 118 | 122 | 126 | 127 | 128 | 129 | 132 | 136 | 140 | 144 | 145 | 146 | 147 | 151 | 155 | 160 | 165 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /test/source/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/fa-regular-400.woff2 -------------------------------------------------------------------------------- /test/source/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/favicon.ico -------------------------------------------------------------------------------- /test/source/heresy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/heresy.png -------------------------------------------------------------------------------- /test/source/index.css: -------------------------------------------------------------------------------- 1 | /* comment */ 2 | body { 3 | font-family: sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /test/source/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ucompress 7 | 8 | 9 | Hello World 10 | 11 | -------------------------------------------------------------------------------- /test/source/index.js: -------------------------------------------------------------------------------- 1 | function test() { 2 | console.log('test'); 3 | } -------------------------------------------------------------------------------- /test/source/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucdn", 3 | "version": "0.19.1", 4 | "description": "A µcompress based CDN utility, compatible with both Express and native http module", 5 | "bin": "./server.cjs", 6 | "main": "./cjs/index.js", 7 | "scripts": { 8 | "build": "npm run cjs && npm run test", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 11 | "test": "rm -rf test/dest && rm -rf /tmp/ucdn && nyc node test/index.js && rm test/index.test" 12 | }, 13 | "keywords": [ 14 | "ucompress", 15 | "CDN", 16 | "Express", 17 | "http" 18 | ], 19 | "author": "Andrea Giammarchi", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "ascjs": "^4.0.1", 23 | "coveralls": "^3.1.0", 24 | "nyc": "^15.1.0" 25 | }, 26 | "module": "./esm/index.js", 27 | "type": "module", 28 | "exports": { 29 | "import": "./esm/index.js", 30 | "default": "./cjs/index.js" 31 | }, 32 | "dependencies": { 33 | "essential-md": "^0.3.1", 34 | "id-promise": "^0.1.0", 35 | "ucdn-utils": "^0.4.13", 36 | "ucompress": "^0.20.3" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/WebReflection/ucdn.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/WebReflection/ucdn/issues" 44 | }, 45 | "homepage": "https://github.com/WebReflection/ucdn#readme" 46 | } 47 | -------------------------------------------------------------------------------- /test/source/rpi2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/rpi2.png -------------------------------------------------------------------------------- /test/source/test.md: -------------------------------------------------------------------------------- 1 | # just MD 2 | 3 | with text 4 | 5 | ```js 6 | while (code) { 7 | isHereToo++; 8 | } 9 | ``` 10 | -------------------------------------------------------------------------------- /test/source/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum vel mi non eros lobortis gravida. Ut non faucibus sapien, a ornare ex. Aliquam arcu turpis, varius et scelerisque ac, hendrerit quis ligula. Nulla sollicitudin egestas mi, vitae rhoncus diam vestibulum vel. Vestibulum posuere ligula dui. Fusce vehicula risus in maximus sollicitudin. In facilisis dui ac sem porttitor faucibus. Mauris vel lorem sed ante mattis bibendum. Curabitur sed ex sed quam vestibulum dictum. 2 | 3 | Sed est tortor, tempor id diam non, malesuada tincidunt tellus. In in condimentum nisi, eu ultricies nunc. Donec nec ornare sapien, in feugiat elit. Donec sed semper justo. Etiam sed aliquet nisi, et condimentum ligula. Fusce interdum, quam ac molestie porttitor, purus nisi auctor nunc, id dapibus orci urna sed augue. Sed non pretium ligula. Phasellus velit mauris, rhoncus eget tellus et, iaculis euismod metus. Nunc pharetra lacinia purus. Praesent a purus aliquam, facilisis nisl vitae, efficitur turpis. Aenean tincidunt sodales elit vitae facilisis. Fusce ac placerat ante. Pellentesque quis massa tempor, posuere elit ut, dignissim lectus. Quisque blandit, tellus id placerat rhoncus, nunc eros facilisis neque, ut efficitur tellus dolor vel tortor. Donec a posuere sapien, ut consectetur ipsum. 4 | 5 | Sed varius orci sed mauris dictum, non ullamcorper urna eleifend. Phasellus ac cursus enim. Nullam est erat, fringilla aliquet pellentesque vel, efficitur nec lorem. Donec in dui et turpis congue gravida. Curabitur suscipit, enim eget imperdiet scelerisque, mauris nibh fringilla mi, ut rutrum justo enim ut elit. Aenean sed erat suscipit, vehicula ex a, gravida purus. Praesent nec ipsum nisl. Quisque interdum purus id est iaculis eleifend. Etiam sagittis aliquam erat, ut sagittis dolor porttitor sed. Mauris purus sapien, tincidunt eu neque eget, tristique pellentesque risus. Sed dictum accumsan tellus id volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec tempor, metus id bibendum ullamcorper, metus libero faucibus ante, sed hendrerit risus eros non mi. 6 | 7 | Aliquam hendrerit ultrices eros vitae porttitor. Ut porttitor, felis at molestie venenatis, odio quam accumsan ligula, id congue tellus tortor pellentesque tellus. Aliquam erat volutpat. Curabitur ut lectus volutpat, cursus sem eget, sollicitudin neque. Aliquam condimentum, nisi dapibus rutrum rhoncus, mi nibh pharetra est, a aliquam metus lorem in ante. Etiam quis dui tincidunt, tincidunt lectus vitae, gravida quam. Etiam non tempus metus, ac ultricies nibh. Cras suscipit posuere urna at consectetur. Pellentesque hendrerit nec eros quis tempor. Mauris egestas turpis risus, ut commodo nisi vulputate sit amet. Praesent fermentum ligula ac risus placerat congue at ac est. Praesent tincidunt sem et nisi suscipit, nec consequat sem laoreet. Curabitur at egestas dui, eu placerat erat. 8 | 9 | Sed suscipit suscipit mi, in laoreet nisl eleifend quis. Interdum et malesuada fames ac ante ipsum primis in faucibus. In hac habitasse platea dictumst. Sed tincidunt aliquet enim, vitae ultrices mauris faucibus vitae. Nulla ornare nisi sit amet ultricies lacinia. Sed volutpat malesuada magna at pulvinar. Nullam congue vel enim rhoncus hendrerit. Maecenas porta est at eleifend viverra. Vestibulum at orci tempus tellus ornare convallis vel non diam. -------------------------------------------------------------------------------- /test/source/wiki-world.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/source/wiki-world.gif -------------------------------------------------------------------------------- /test/ucdn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucdn/c07efcd5207d69d8526c24f34a973a8670d63bf5/test/ucdn.jpg --------------------------------------------------------------------------------