├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── babel.config.js ├── package.json ├── rollup.config.js └── src ├── cli.js ├── index.js └── lib ├── argo.js ├── colors.js ├── configs-normalize.js ├── configs-touch.js ├── configs-uses.js ├── configs.js ├── get-options-certificate.js ├── get-options-config.js ├── get-options.js ├── get-path-stats.js ├── messages.js ├── server-get-first-available-port.js ├── server.js ├── touch-file-as.js └── util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.editorconfig 3 | !.gitignore 4 | !.travis.yml 5 | *.log* 6 | /localhost.* 7 | /cli.* 8 | /index.* 9 | node_modules 10 | package-lock.json 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/travis-lint 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 6 7 | 8 | install: 9 | - npm install --ignore-scripts 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upsite [][Upsite] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Support Chat][git-img]][git-url] 6 | 7 | [Upsite] lets you start a local server without any configuration. 8 | 9 | ```bash 10 | npx upsite 11 | ``` 12 | 13 | That’s it. Your website is up on HTTP and HTTPs. Need a trusted certificate? 14 | 15 | ```bash 16 | npx upsite --trust 17 | ``` 18 | 19 | That’s it. Your HTTPs certificate is trusted and you won’t need to run this 20 | command again. Need a custom port? 21 | 22 | ```bash 23 | npx upsite --port 8080 24 | ``` 25 | 26 | That’s it. Your website is up on HTTP and HTTPs at port _8080_. Need [Babel], 27 | [PostCSS], or [pHTML]? 28 | 29 | ```bash 30 | npx upsite --config standard 31 | ``` 32 | 33 | That’s it. HTML, CSS, and JS files in the _public_ directory are automatically 34 | transformed. Need to change that directory? 35 | 36 | ```bash 37 | npx update --dir whatever 38 | ``` 39 | 40 | That’s it. Your files are served from the _whatever_ directory. Need your own 41 | rules? 42 | 43 | ```bash 44 | npx update --config whatever 45 | ``` 46 | 47 | That’s it. You control the configuration from _whatever.config.js_. 48 | 49 | ```js 50 | module.exports = { 51 | dir: 'www', 52 | uses: [ 53 | // use js/jsx files with babel 54 | { 55 | extensions: ['js', 'jsx'], 56 | require: { 57 | babel: '@babel/core' 58 | }, 59 | config: { 60 | plugins: [ 61 | ['@babel/plugin-transform-react-jsx', { 62 | pragma: '$', 63 | pragmaFrag: '$', 64 | useBuiltIns: true 65 | }] 66 | ] 67 | }, 68 | // transform jsx files with babel 69 | write: (use, stats, opts) => use.require.babel.transformAsync( 70 | stats.source, 71 | { 72 | ...use.config, 73 | babelrc: false, 74 | filename: stats.pathname 75 | } 76 | ).then( 77 | result => result.code 78 | ) 79 | }, 80 | // use svg files with svgo 81 | { 82 | extensions: ['svg'], 83 | require: { 84 | svgo: 'svgo' 85 | }, 86 | // use package.json[svgo], .svgorc, .svgorc.json, .svgorc.yaml, .svgorc.yml, .svgorc.js, svgo.config.js 87 | config: (use) => use.readConfig('svgo'), 88 | // transform svg files with svgo 89 | write: (use, stats) => new use.require.svgo().optimize( 90 | stats.source, 91 | { 92 | ...use.config, 93 | path: stats.filepath 94 | } 95 | ).then( 96 | result => result.data 97 | ) 98 | } 99 | ] 100 | }; 101 | ``` 102 | 103 | --- 104 | 105 | [Upsite] automatically spins up a server and lets you start writing to files. 106 | The server includes a self-signed SSL certificate which can be trusted. Upsite 107 | creates a `package.json` file and a `public` folder with HTML, CSS, and JS 108 | files inside of it to get you started, but these can be changed as well. 109 | 110 | [cli-img]: https://img.shields.io/travis/jonathantneal/upsite.svg 111 | [cli-url]: https://travis-ci.org/jonathantneal/upsite 112 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 113 | [git-url]: https://gitter.im/postcss/postcss 114 | [npm-img]: https://img.shields.io/npm/v/upsite.svg 115 | [npm-url]: https://www.npmjs.com/package/upsite 116 | 117 | [Babel]: https://github.com/babel/babel/ 118 | [Express]: http://expressjs.com/ 119 | [Express Variable]: https://github.com/jonathantneal/express-variable 120 | [pHTML]: https://github.com/phtmlorg/phtml 121 | [PostCSS]: https://github.com/postcss/postcss 122 | [Upsite]: https://github.com/jonathantneal/upsite 123 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | ['@babel/plugin-proposal-class-properties', { 4 | loose: true 5 | }] 6 | ], 7 | presets: [ 8 | ['@babel/preset-env', { 9 | corejs: 3, 10 | loose: true, 11 | modules: false, 12 | targets: { node: 6 }, 13 | useBuiltIns: 'entry' 14 | }] 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upsite", 3 | "version": "0.10.0", 4 | "description": "Just put up a site", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": "jonathantneal/upsite", 8 | "homepage": "https://github.com/jonathantneal/upsite#readme", 9 | "bugs": "https://github.com/jonathantneal/upsite/issues", 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "bin": { 13 | "upsite": "cli.js" 14 | }, 15 | "files": [ 16 | "cli.js", 17 | "index.js" 18 | ], 19 | "scripts": { 20 | "build": "npm run build:cli && npm run build:node", 21 | "build:cli": "cross-env NODE_ENV=cli rollup --config --silent", 22 | "build:node": "cross-env NODE_ENV=node rollup --config --silent", 23 | "prepublishOnly": "npm test && npm run build", 24 | "test": "npm run test:js", 25 | "test:js": "eslint src/*.js src/lib/*.js --cache --ignore-path .gitignore --quiet" 26 | }, 27 | "engines": { 28 | "node": ">=6.0.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.4.3", 32 | "@babel/plugin-proposal-class-properties": "^7.4.0", 33 | "@babel/preset-env": "^7.4.3", 34 | "babel-eslint": "^10.0.1", 35 | "cross-env": "^5.2.0", 36 | "eslint": "^5.16.0", 37 | "eslint-config-dev": "^2.0.0", 38 | "fse": "^4.0.1", 39 | "js-yaml": "^3.13.0", 40 | "mime-types": "^2.1.22", 41 | "require-from-string": "^2.0.2", 42 | "resolve": "^1.10.0", 43 | "rollup": "^1.8.0", 44 | "rollup-plugin-babel": "^4.3.2", 45 | "rollup-plugin-commonjs": "^9.3.0", 46 | "rollup-plugin-json": "^4.0.0", 47 | "rollup-plugin-node-resolve": "^4.0.1", 48 | "rollup-plugin-terser": "^4.0.4", 49 | "selfsigned": "^1.10.4" 50 | }, 51 | "eslintConfig": { 52 | "extends": "dev", 53 | "parser": "babel-eslint", 54 | "rules": { 55 | "consistent-return": [ 56 | 0 57 | ] 58 | } 59 | }, 60 | "keywords": [] 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import json from 'rollup-plugin-json'; 4 | import nodeResolve from 'rollup-plugin-node-resolve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const isCli = String(process.env.NODE_ENV).includes('cli'); 8 | 9 | const input = isCli ? 'src/cli.js' : 'src/index.js'; 10 | const output = isCli ? { file: 'cli.js', format: 'cjs', strict: false } : [{ file: 'index.js', format: 'cjs', strict: false }, { file: 'index.mjs', format: 'esm', strict: false }]; 11 | const plugins = [ 12 | babel(), 13 | nodeResolve(), 14 | commonjs(), 15 | json(), 16 | terser() 17 | ].concat(isCli ? addHashBang() : []) 18 | 19 | export default { input, output, plugins }; 20 | 21 | function addHashBang() { 22 | return { 23 | name: 'add-hash-bang', 24 | renderChunk(code) { 25 | return `#!/usr/bin/env node\n${code}`; 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import argo from './lib/argo'; 2 | import upsite from '.'; 3 | 4 | upsite(argo); 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fse from 'fse'; 2 | import getOptions from './lib/get-options'; 3 | import getPathStats from './lib/get-path-stats'; 4 | import msgs from './lib/messages'; 5 | import Server from './lib/server'; 6 | import { parse as parseURL } from 'url'; 7 | import { require_config } from './lib/util'; 8 | 9 | export default function upsite(rawopts) { 10 | getOptions(rawopts).then(opts => { 11 | new Server({ cert: opts.cert, key: opts.key }, requestListener).listen(opts.port).then(servers => { 12 | msgs.isReady(servers.map(server => server.port)); 13 | }); 14 | 15 | function requestListener(request, response) { 16 | const location = parseURL(request.url); 17 | 18 | return getPathStats(opts.dir, location.pathname).then(stats => { 19 | const isTheFileUnmodified = request.headers['if-modified-since'] === stats.lastModified; 20 | 21 | if (isTheFileUnmodified) { 22 | response.writeHead(304); 23 | 24 | return response.end(); 25 | } 26 | 27 | if (request.method === 'HEAD') { 28 | response.writeHead(200, { 29 | 'Connection': 'keep-alive', 30 | 'Content-Length': stats.size, 31 | 'Content-Type': stats.contentType, 32 | 'Date': stats.date, 33 | 'Last-Modified': stats.lastModified 34 | }); 35 | 36 | return response.end(); 37 | } else { 38 | if (request.method === 'GET') { 39 | for (const use of opts.uses) { 40 | if (use.extensions.includes(stats.extname)) { 41 | // ... 42 | Object.assign(stats, { location }); 43 | 44 | // eslint-disable-next-line no-loop-func 45 | return fse.readFile(stats.pathname, 'utf8').then(source => { 46 | Object.assign(stats, { source }); 47 | 48 | // ... 49 | return Promise.resolve(use.write(use, stats, opts)).then(buffer => { 50 | response.writeHead(200, { 51 | 'Connection': 'keep-alive', 52 | 'Content-Type': stats.contentType, 53 | 'Content-Length': buffer.length, 54 | 'Date': stats.date, 55 | 'Last-Modified': stats.lastModified 56 | }); 57 | 58 | response.write(buffer); 59 | 60 | response.end(); 61 | }); 62 | }).catch(error => { 63 | response.writeHead(404); 64 | 65 | response.write(String(Object(error).message || '') || String(error || '')); 66 | 67 | return response.end(); 68 | }); 69 | } 70 | } 71 | } 72 | 73 | response.writeHead(200, { 74 | 'Connection': 'keep-alive', 75 | 'Content-Type': stats.contentType, 76 | 'Content-Length': stats.size, 77 | 'Date': stats.date, 78 | 'Last-Modified': stats.lastModified 79 | }); 80 | 81 | const readStream = fse.createReadStream(stats.pathname); 82 | 83 | return readStream.pipe(response); 84 | } 85 | }).catch(error => { 86 | response.writeHead(404); 87 | response.write(String(Object(error).message || '') || String(error || '')); 88 | 89 | return response.end(); 90 | }); 91 | } 92 | }, msgs.isNotAvailable); 93 | } 94 | 95 | export { require_config as readConfig } 96 | -------------------------------------------------------------------------------- /src/lib/argo.js: -------------------------------------------------------------------------------- 1 | const defaultOpts = { 2 | config: '', 3 | dir: 'public', 4 | trust: false 5 | }; 6 | 7 | const namelessArgs = [ 8 | 'dir', 9 | 'port', 10 | 'config' 11 | ]; 12 | 13 | const shorthandArgs = { 14 | '-c': '--config', 15 | '-d': '--dir', 16 | '-p': '--port', 17 | '-t': '--trust' 18 | }; 19 | 20 | const dash = /^--([^\s]+)$/; 21 | 22 | export default Object.assign({ 23 | port: [80, 443] 24 | }, process.argv.slice(2).reduce( 25 | // eslint-disable-next-line max-params 26 | (object, arg, i, args) => { 27 | if (dash.test(getArgName(arg))) { 28 | // handle name/value arguments 29 | const argName = getArgName(arg).replace(dash, '$1'); 30 | 31 | object[argName] = i + 1 in args 32 | ? getInstance(argName, args[i + 1], object) 33 | : true; 34 | } else if (!dash.test(getArgName(args[i - 1]))) { 35 | // handle nameless value arguments 36 | namelessArgs.some((namelessArg, namelessIndex) => { 37 | if (object[namelessArg] === defaultOpts[namelessArg]) { 38 | object[namelessArg] = getInstance(namelessArg, arg, object); 39 | 40 | namelessArgs.splice(namelessIndex, 1); 41 | 42 | return true; 43 | } 44 | 45 | return false; 46 | }); 47 | } 48 | 49 | return object; 50 | }, 51 | { 52 | ...defaultOpts 53 | } 54 | )); 55 | 56 | function getArgName(arg) { 57 | return shorthandArgs[arg] || arg; 58 | } 59 | 60 | function getInstance(argName, arg, object) { 61 | const { constructor } = defaultOpts[argName] || ''; 62 | 63 | if (argName === 'port') { 64 | const ports = arg.split(/,/g).map(argPort => Number(argPort)); 65 | 66 | return object.port ? object.port.concat(ports) : ports; 67 | } else if (Boolean === constructor) { 68 | return arg !== 'false'; 69 | } else if ([Object,Number,String].includes(constructor)) { 70 | return constructor(arg); 71 | } else { 72 | return new constructor(arg); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/colors.js: -------------------------------------------------------------------------------- 1 | 2 | // color strings 3 | const colors = { 4 | bold: '\x1b[1m', 5 | dim: '\x1b[2m', 6 | underline: '\x1b[4m', 7 | blink: '\x1b[5m', 8 | reverse: '\x1b[7m', 9 | hidden: '\x1b[8m', 10 | black: '\x1b[30m', 11 | red: '\x1b[31m', 12 | green: '\x1b[32m', 13 | yellow: '\x1b[33m', 14 | blue: '\x1b[34m', 15 | magenta: '\x1b[35m', 16 | cyan: '\x1b[36m', 17 | white: '\x1b[37m', 18 | bgBlack: '\x1b[40m', 19 | bgRed: '\x1b[41m', 20 | bgGreen: '\x1b[42m', 21 | bgYellow: '\x1b[43m', 22 | bgBlue: '\x1b[44m', 23 | bgMagenta: '\x1b[45m', 24 | bgCyan: '\x1b[46m', 25 | bgWhite: '\x1b[47m' 26 | }; 27 | 28 | const reset = '\x1b[0m'; 29 | 30 | Object.keys(colors).forEach(name => { 31 | const color = colors[name]; 32 | 33 | colors[name] = string => color + string.replace(reset, reset + color) + reset; 34 | }); 35 | 36 | export default colors; 37 | 38 | export const bold = colors.bold; 39 | export const dim = colors.dim; 40 | export const underline = colors.underline; 41 | export const blink = colors.blink; 42 | export const reverse = colors.reverse; 43 | export const hidden = colors.hidden; 44 | export const black = colors.black; 45 | export const red = colors.red; 46 | export const green = colors.green; 47 | export const yellow = colors.yellow; 48 | export const blue = colors.blue; 49 | export const magenta = colors.magenta; 50 | export const cyan = colors.cyan; 51 | export const white = colors.white; 52 | export const bgBlack = colors.bgBlack; 53 | export const bgRed = colors.bgRed; 54 | export const bgGreen = colors.bgGreen; 55 | export const bgYellow = colors.bgYellow; 56 | export const bgBlue = colors.bgBlue; 57 | export const bgMagenta = colors.bgMagenta; 58 | export const bgCyan = colors.bgCyan; 59 | export const bgWhite = colors.bgWhite; 60 | -------------------------------------------------------------------------------- /src/lib/configs-normalize.js: -------------------------------------------------------------------------------- 1 | import { required } from './util'; 2 | 3 | export default function normalizePlugins(plugins, pluginIdTransformer, requireOpts) { 4 | const requireCache = {}; 5 | 6 | let promise = Promise.resolve(); 7 | 8 | plugins.forEach((plugin, index) => { 9 | if (typeof plugin === 'string') { 10 | const expandedId = typeof pluginIdTransformer === 'function' ? pluginIdTransformer(plugin) : plugin; 11 | 12 | promise = promise.then( 13 | () => required(expandedId, requireOpts, requireCache).catch(() => required(plugin, requireOpts, requireCache)) 14 | ).then(exported => { 15 | plugins[index] = exported; 16 | }, () => {}); 17 | } else if (Array.isArray(plugin) && typeof plugin[0] === 'string') { 18 | const expandedId = typeof pluginIdTransformer === 'function' ? pluginIdTransformer(plugin[0]) : plugin[0]; 19 | 20 | promise = promise.then( 21 | () => required(expandedId, requireOpts, requireCache).catch(() => required(plugin[0], requireOpts, requireCache)).catch(() => plugin[0]) 22 | ).then(exported => { 23 | if (1 in plugin) { 24 | plugins[index] = typeof exported === 'function' 25 | ? exported(plugin[1]) 26 | : [exported, plugin[1]]; 27 | } 28 | }, () => {}); 29 | } 30 | }); 31 | 32 | return promise.then(() => plugins); 33 | } 34 | 35 | export function normalizeBabelPluginId(id) { 36 | return id.replace(/^(@[^\/]+\/)(?!plugin)/, '$1plugin-').replace(/^(?!@|babel-plugin)/, 'babel-plugin-'); 37 | } 38 | 39 | export function normalizeBabelPresetId(id) { 40 | return id.replace(/^(@[^\/]+\/)(?!preset)/, '$1preset-').replace(/^(?!@|babel-preset)/, 'babel-preset-'); 41 | } 42 | 43 | export function normalizePhtmlPluginId(id) { 44 | return id.replace(/^(@(?!phtml)\/)(?!phtml)/, '$phtml-').replace(/^(?!@|phtml-)/, 'phtml-'); 45 | } 46 | 47 | export function normalizePostcssPluginId(id) { 48 | return id.replace(/^(@[^\/]+\/)(?!postcss)/, '$1postcss-').replace(/^(?!@|postcss-)/, 'postcss-'); 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/configs-touch.js: -------------------------------------------------------------------------------- 1 | /* Touches 2 | /* ========================================================================== */ 3 | 4 | const touchDefaultBrowserslist = `# browsers that we support 5 | 6 | last 2 versions 7 | not dead`; 8 | 9 | const touchDefaultIndexHtml = ` 10 | upsite 11 | 12 | 13 | 14 |

upsite

15 | `; 16 | 17 | const touchDefaultStyleCss = `body { 18 | margin-left: 5%; 19 | margin-right: 5%; 20 | }`; 21 | 22 | const touchDefaultScriptJs = `document.addEventListener('DOMContentLoaded', function () { 23 | document.body.appendChild( 24 | document.createElement('p') 25 | ).appendChild( 26 | document.createTextNode('Greetings, ' + navigator.vendor + ' browser!') 27 | ); 28 | })`; 29 | 30 | const touchDefault = { 31 | '${dir}/index.html': touchDefaultIndexHtml, 32 | '${dir}/script.js': touchDefaultScriptJs, 33 | '${dir}/style.css': touchDefaultStyleCss 34 | }; 35 | 36 | /* Touches for Phtml 37 | /* ========================================================================== */ 38 | 39 | const touchPhtmlIndexHtml = `upsite with phtml 40 | 41 | 42 | 43 |

upsite with phtml

44 | `; 45 | 46 | const touchPhtml = { 47 | '.browserslist': touchDefaultBrowserslist, 48 | '${dir}/index.html': touchPhtmlIndexHtml, 49 | '${dir}/script.js': touchDefaultScriptJs, 50 | '${dir}/style.css': touchDefaultStyleCss 51 | }; 52 | 53 | /* Touches for PostCSS 54 | /* ========================================================================== */ 55 | 56 | const touchPostcssIndexHtml = ` 57 | upsite with postcss 58 | 59 | 60 | 61 |

upsite with postcss

62 | `; 63 | 64 | const touchPostcssStyleCss = `body { 65 | margin-inline: 5%; 66 | }`; 67 | 68 | const touchPostcss = { 69 | '.browserslist': touchDefaultBrowserslist, 70 | '${dir}/index.html': touchPostcssIndexHtml, 71 | '${dir}/script.js': touchDefaultScriptJs, 72 | '${dir}/style.css': touchPostcssStyleCss 73 | }; 74 | 75 | /* Touches for JSX 76 | /* ========================================================================== */ 77 | 78 | const touchJsxIndexHtml = ` 79 | upsite with jsx 80 | 81 | 82 | 83 | 84 |

upsite with jsx

85 | `; 86 | 87 | const touchJsxJsxJs = `function $(node, props) { 88 | const element = node === $ ? document.createDocumentFragment() : node instanceof Node ? node : document.createElement(node); 89 | for (const prop in Object(props)) /^on/.test(prop) ? element.addEventListener(prop.slice(2), props[prop]) : e.setAttribute(prop, props[prop]); 90 | for (let child = Array.prototype.slice.call(arguments, 2), i = -1; ++i in child; ) element.appendChild(typeof child[i] === 'string' ? document.createTextNode(child[i]) : child[i]); 91 | return element; 92 | }`; 93 | 94 | const touchJsxScriptJs = `document.addEventListener('DOMContentLoaded', () => { 95 | document.body.append(

Greetings, {navigator.vendor} browser!

) 96 | })`; 97 | 98 | const touchJsx = { 99 | '.browserslist': touchDefaultBrowserslist, 100 | '${dir}/index.html': touchJsxIndexHtml, 101 | '${dir}/jsx.js': touchJsxJsxJs, 102 | '${dir}/script.js': touchJsxScriptJs, 103 | '${dir}/style.css': touchDefaultStyleCss 104 | }; 105 | 106 | /* Touches for React 107 | /* ========================================================================== */ 108 | 109 | const touchReactIndexHtml = ` 110 | upsite with react 111 | 112 | 113 | 114 | 115 |
`; 116 | 117 | const touchReactScriptJs = `document.addEventListener('DOMContentLoaded', () => { 118 | ReactDOM.render(<> 119 |

upsite

120 |

Greetings, {navigator.vendor} browser!

121 | , document.getElementById('root')) 122 | })`; 123 | 124 | const touchReact = { 125 | '.browserslist': touchDefaultBrowserslist, 126 | '${dir}/index.html': touchReactIndexHtml, 127 | '${dir}/script.js': touchReactScriptJs, 128 | '${dir}/style.css': touchDefaultStyleCss 129 | }; 130 | 131 | /* Touches for CRA 132 | /* ========================================================================== */ 133 | 134 | const touchCra = { 135 | '.browserslist': touchDefaultBrowserslist, 136 | '${dir}/index.html': touchReactIndexHtml, 137 | '${dir}/script.js': touchReactScriptJs, 138 | '${dir}/style.css': touchPostcssStyleCss 139 | }; 140 | 141 | /* Touches for Standard 142 | /* ========================================================================== */ 143 | 144 | const touchStandardIndexHtml = `upsite 145 | 146 | 147 | 148 | 149 |

upsite

150 | `; 151 | 152 | const touchStandard = { 153 | '.browserslist': touchDefaultBrowserslist, 154 | '${dir}/index.html': touchStandardIndexHtml, 155 | '${dir}/jsx.js': touchJsxJsxJs, 156 | '${dir}/script.js': touchJsxScriptJs, 157 | '${dir}/style.css': touchPostcssStyleCss 158 | }; 159 | 160 | /* Export Touches 161 | /* ========================================================================== */ 162 | 163 | export default { 164 | cra: touchCra, 165 | default: touchDefault, 166 | jsx: touchJsx, 167 | phtml: touchPhtml, 168 | postcss: touchPostcss, 169 | react: touchReact, 170 | standard: touchStandard 171 | }; 172 | -------------------------------------------------------------------------------- /src/lib/configs-uses.js: -------------------------------------------------------------------------------- 1 | import msgs from './messages'; 2 | import path from 'path'; 3 | import normalizePlugins, { normalizeBabelPluginId, normalizeBabelPresetId, normalizePhtmlPluginId, normalizePostcssPluginId } from './configs-normalize'; 4 | import { touchPackageJson } from './touch-file-as'; 5 | 6 | const requiredOpts = { 7 | npmInstallOptions: '--save-dev', 8 | onBeforeNpmInstall: msgs.isInstalling 9 | }; 10 | 11 | /* Uses for Phtml 12 | /* ========================================================================== */ 13 | 14 | const usePhtml = { 15 | extensions: ['htm', 'html', 'phtml'], 16 | require: { 17 | phtml: 'phtml' 18 | }, 19 | config(use) { 20 | return touchPackageJson().then( 21 | () => use.readConfig('phtml') 22 | ).then( 23 | nextConfig => ({ 24 | plugins: [ 25 | ['@phtml/doctype', { 26 | safe: true 27 | }], 28 | '@phtml/include', 29 | '@phtml/jsx', 30 | '@phtml/markdown', 31 | '@phtml/schema', 32 | '@phtml/self-closing' 33 | ], 34 | ...Object(nextConfig) 35 | }) 36 | ).then( 37 | nextConfig => normalizePlugins( 38 | nextConfig.plugins || [], 39 | normalizePhtmlPluginId, 40 | requiredOpts 41 | ).then(normalizedPlugins => ({ 42 | ...nextConfig, 43 | plugins: normalizedPlugins 44 | })) 45 | ); 46 | }, 47 | write(use, stats) { 48 | // configure phtml process options 49 | const processOpts = { ...use.config, from: stats.pathname }; 50 | 51 | delete processOpts.plugins; 52 | 53 | return use.require.phtml.use(use.config.plugins).process(stats.source, processOpts).then( 54 | result => result.html 55 | ); 56 | } 57 | }; 58 | 59 | /* Uses for PostCSS 60 | /* ========================================================================== */ 61 | 62 | const usePostcss = { 63 | extensions: ['css', 'pcss'], 64 | require: { 65 | postcss: 'postcss' 66 | }, 67 | config(use) { 68 | return touchPackageJson().then( 69 | () => use.readConfig('postcss') 70 | ).then( 71 | nextConfig => ({ 72 | plugins: [ 73 | 'import', 74 | ['preset-env', { 75 | stage: 0 76 | }] 77 | ], 78 | map: { 79 | inline: true 80 | }, 81 | ...Object(nextConfig) 82 | }) 83 | ).then( 84 | nextConfig => normalizePlugins( 85 | nextConfig.plugins || [], 86 | normalizePostcssPluginId, 87 | requiredOpts 88 | ).then(normalizedPlugins => ({ 89 | ...nextConfig, 90 | plugins: normalizedPlugins 91 | })) 92 | ); 93 | }, 94 | write(use, stats) { 95 | // configure postcss process options 96 | const processOpts = { ...use.config, from: stats.pathname, to: stats.pathname }; 97 | 98 | delete processOpts.plugins; 99 | 100 | const plugins = use.config.plugins.length ? use.config.plugins : [() => {}]; 101 | 102 | // process the source using postcss 103 | return use.require.postcss(plugins).process(stats.source, processOpts).then( 104 | result => result.css 105 | ); 106 | } 107 | }; 108 | 109 | /* Uses for JSX 110 | /* ========================================================================== */ 111 | 112 | const useJsx = { 113 | extensions: ['js', 'jsx'], 114 | require: { 115 | babel: '@babel/core' 116 | }, 117 | config(use) { 118 | return touchPackageJson().then( 119 | () => use.readConfig('babel') 120 | ).then( 121 | nextConfig => ({ 122 | plugins: [ 123 | ['@babel/proposal-class-properties', { 124 | loose: true 125 | }], 126 | ['@babel/plugin-transform-react-jsx', { 127 | pragma: '$', 128 | pragmaFrag: '$', 129 | useBuiltIns: true 130 | }] 131 | ], 132 | presets: [ 133 | ['@babel/env', { 134 | loose: true, 135 | modules: false, 136 | useBuiltIns: 'entry' 137 | }] 138 | ], 139 | sourceMaps: 'inline', 140 | ...Object(nextConfig) 141 | }) 142 | ).then( 143 | nextConfig => Promise.all([ 144 | normalizePlugins( 145 | nextConfig.plugins || [], 146 | normalizeBabelPluginId, 147 | requiredOpts 148 | ), 149 | normalizePlugins( 150 | nextConfig.presets || [], 151 | normalizeBabelPresetId, 152 | requiredOpts 153 | ) 154 | ]).then(([normalizedPlugins, normalizedPresets]) => ({ 155 | ...nextConfig, 156 | plugins: normalizedPlugins, 157 | presets: normalizedPresets 158 | })) 159 | ); 160 | }, 161 | write(use, stats, opts) { 162 | // configure babel transform options 163 | const transformOpts = { ...use.config, babelrc: false, filename: stats.pathname, filenameRelative: path.relative(opts.dir, stats.pathname) }; 164 | 165 | // process the source using babel 166 | return use.require.babel.transformAsync(stats.source, transformOpts).then( 167 | result => result.code 168 | ); 169 | } 170 | }; 171 | 172 | /* Uses for React 173 | /* ========================================================================== */ 174 | 175 | const useReact = { 176 | ...useJsx, 177 | config(use) { 178 | return touchPackageJson().then( 179 | () => use.readConfig('babel') 180 | ).then( 181 | nextConfig => ({ 182 | plugins: [ 183 | ['@babel/proposal-class-properties', { 184 | loose: true 185 | }] 186 | ], 187 | presets: [ 188 | ['@babel/preset-react', { 189 | useBuiltIns: true 190 | }], 191 | ['@babel/env', { 192 | loose: true, 193 | modules: false, 194 | useBuiltIns: 'entry' 195 | }] 196 | ], 197 | sourceMaps: 'inline', 198 | ...Object(nextConfig) 199 | }) 200 | ).then( 201 | nextConfig => Promise.all([ 202 | normalizePlugins( 203 | nextConfig.plugins || [], 204 | normalizeBabelPluginId, 205 | requiredOpts 206 | ), 207 | normalizePlugins( 208 | nextConfig.presets || [], 209 | normalizeBabelPresetId, 210 | requiredOpts 211 | ) 212 | ]).then(([normalizedPlugins, normalizedPresets]) => ({ 213 | ...nextConfig, 214 | plugins: normalizedPlugins, 215 | presets: normalizedPresets 216 | })) 217 | ); 218 | } 219 | }; 220 | 221 | /* Export Uses 222 | /* ========================================================================== */ 223 | 224 | export default { 225 | cra: [ usePostcss, useReact ], 226 | default: [], 227 | jsx: [ useJsx ], 228 | phtml: [ usePhtml ], 229 | postcss: [ usePostcss ], 230 | react: [ useReact ], 231 | standard: [ usePhtml, usePostcss, useJsx ] 232 | }; 233 | -------------------------------------------------------------------------------- /src/lib/configs.js: -------------------------------------------------------------------------------- 1 | import touch from './configs-touch'; 2 | import uses from './configs-uses'; 3 | 4 | export default { 5 | cra: { 6 | uses: uses.cra, 7 | touch: touch.cra 8 | }, 9 | default: { 10 | uses: uses.default, 11 | touch: touch.default 12 | }, 13 | empty: { 14 | uses: uses.empty, 15 | touch: touch.empty 16 | }, 17 | jsx: { 18 | uses: uses.jsx, 19 | touch: touch.jsx 20 | }, 21 | phtml: { 22 | uses: uses.phtml, 23 | touch: touch.phtml 24 | }, 25 | postcss: { 26 | uses: uses.postcss, 27 | touch: touch.postcss 28 | }, 29 | react: { 30 | uses: uses.react, 31 | touch: touch.react 32 | }, 33 | standard: { 34 | uses: uses.standard, 35 | touch: touch.standard 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/get-options-certificate.js: -------------------------------------------------------------------------------- 1 | import { exec } from './util'; 2 | import fse from 'fse'; 3 | import msgs from './messages'; 4 | import path from 'path'; 5 | import { generate } from 'selfsigned'; 6 | 7 | const certPathname = path.resolve(__dirname, 'localhost.crt'); 8 | const keyPathname = path.resolve(__dirname, 'localhost.key'); 9 | 10 | /** 11 | * @function getCertificate 12 | * @return {Certificate} 13 | */ 14 | 15 | export default function getCertificateOpts(opts) { 16 | return Promise.all([ 17 | // read the existing certificates 18 | fse.readFile(certPathname, 'utf8'), 19 | fse.readFile(keyPathname, 'utf8') 20 | ]).then( 21 | ([ cert, key ]) => ({ cert, key }), 22 | error => { 23 | if (error.code === 'ENOENT') { 24 | // generate the certificates if they do not exist 25 | const certs = generateCertificate(opts.trust); 26 | 27 | return Promise.all([ 28 | fse.writeFile(certPathname, certs.cert), 29 | fse.writeFile(keyPathname, certs.key) 30 | ]).then(() => certs); 31 | } 32 | 33 | throw error; 34 | } 35 | ).then(certs => { 36 | // conditionally trust the certificates on the system 37 | if (opts.trust) { 38 | if (process.platform === 'win32') { 39 | msgs.isTrustingHttps(); 40 | 41 | return exec(`certutil -addstore -user root "${certPathname}"`).then(() => certs); 42 | } else if (process.platform === 'darwin') { 43 | msgs.isTrustingHttps(); 44 | 45 | return exec(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${certPathname}`).then(() => certs); 46 | } 47 | } 48 | 49 | return certs; 50 | }); 51 | } 52 | 53 | /** 54 | * @function generateCertificate 55 | * @return {Certificate} 56 | */ 57 | 58 | function generateCertificate() { 59 | const { cert, private: key } = generate([ 60 | { name: 'commonName', value: 'localhost' }, 61 | { name: 'countryName', value: 'US' }, 62 | { shortName: 'ST', value: 'California' }, 63 | { name: 'localityName', value: 'Los Angeles' }, 64 | { name: 'organizationName', value: 'Company' } 65 | ], { 66 | extensions: [ 67 | { 68 | name: 'subjectAltName', 69 | altNames: [{ 70 | type: 2, 71 | value: 'localhost' 72 | }, { 73 | type: 7, 74 | ip: '127.0.0.1' 75 | }] 76 | } 77 | ] 78 | }); 79 | 80 | return { cert, key }; 81 | } 82 | 83 | /** 84 | * @typedef {Object} Certificate 85 | * @property {String} cert - self-signed certificate 86 | * @property {String} key - private key of the certificate 87 | */ 88 | -------------------------------------------------------------------------------- /src/lib/get-options-config.js: -------------------------------------------------------------------------------- 1 | import { required, require_config as readConfig } from './util'; 2 | import msgs from './messages'; 3 | import configs from './configs'; 4 | import touchFileAs, { touchPackageJson } from './touch-file-as'; 5 | 6 | export default function getConfigOpts(opts, initialConfig, cache) { 7 | // get the specified config or the default config 8 | const configOpts = { 9 | ...configs.default, 10 | ...Object( 11 | initialConfig || 12 | configs[opts.config] || 13 | ( 14 | Object(opts.config) === opts.config 15 | ? opts.config 16 | : {} 17 | ) 18 | ) 19 | }; 20 | 21 | Object.assign(configOpts, opts, { 22 | config: configOpts.config || {}, 23 | dir: Object(initialConfig).dir || opts.dir 24 | }); 25 | 26 | // normalize config uses as an array 27 | configOpts.uses = [].concat(configOpts.uses || []).map( 28 | use => Object.assign(Object(use), { readConfig }) 29 | ); 30 | 31 | let usePromise = Promise.resolve(); 32 | 33 | configOpts.uses.forEach(use => { 34 | // install and require modules requested by the use 35 | const requireNames = Object.keys(Object(use.require)); 36 | 37 | if (requireNames.length) { 38 | usePromise = usePromise.then(touchPackageJson); 39 | 40 | requireNames.forEach(name => { 41 | const id = use.require[name]; 42 | 43 | usePromise = usePromise.then( 44 | () => required(id, { 45 | npmInstallOptions: '--save-dev', 46 | onBeforeNpmInstall: msgs.isInstalling 47 | }, cache).then(requiredExport => { 48 | use.require[name] = requiredExport; 49 | }) 50 | ); 51 | }); 52 | } 53 | 54 | // run the config function 55 | if (typeof use.config === 'function') { 56 | usePromise = usePromise.then( 57 | () => use.config(use, opts) 58 | ).then(config => { 59 | Object.assign(use, { config }); 60 | }); 61 | } 62 | }); 63 | 64 | // touch any files requested by the config 65 | Object.keys(Object(configOpts.touch)).forEach(filename => { 66 | const normalizedFilename = normalizeFilename(filename, opts); 67 | 68 | usePromise = usePromise.then( 69 | () => touchFileAs(normalizedFilename, configOpts.touch[filename]) 70 | ); 71 | }); 72 | 73 | return usePromise.then(() => configOpts); 74 | } 75 | 76 | function normalizeFilename(filename, opts) { 77 | return filename.replace(/\$\{(\w+)\}/g, ($0, $1) => { 78 | return $1 in opts ? opts[$1] : $0; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /src/lib/get-options.js: -------------------------------------------------------------------------------- 1 | import getCertificateOpts from './get-options-certificate'; 2 | import getConfigOpts from './get-options-config'; 3 | import path from 'path'; 4 | import { cwd } from './util'; 5 | import { require_config } from './util'; 6 | 7 | export default function getOptions(rawopts) { 8 | const initialOpts = { 9 | ...Object(rawopts), 10 | dir: path.resolve(cwd, Object(rawopts).dir || ''), 11 | cwd 12 | }; 13 | const cache = {}; 14 | const configName = initialOpts.config = initialOpts.config || 'default'; 15 | 16 | return require_config(configName).then( 17 | initialConfig => Promise.all([ 18 | getConfigOpts(initialOpts, initialConfig, cache), 19 | getCertificateOpts(initialOpts) 20 | ]) 21 | ).then( 22 | ([ configOpts, certificateOpts ]) => ({ 23 | ...configOpts, 24 | ...certificateOpts 25 | }) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/get-path-stats.js: -------------------------------------------------------------------------------- 1 | import fse from 'fse'; 2 | import mimeTypes from 'mime-types'; 3 | import path from 'path'; 4 | 5 | export default function getPathStats(...pathnames) { 6 | const pathname = path.join(...pathnames); 7 | 8 | return fse.stat(pathname).then(stats => { 9 | if (stats.isDirectory()) { 10 | return getPathStats(path.join(pathname, 'index.html')); 11 | } 12 | 13 | const date = new Date().toUTCString(); 14 | const extname = path.extname(pathname).slice(1).toLowerCase(); 15 | const lastModified = new Date(stats.mtimeMs).toUTCString(); 16 | const contentType = mimeTypes.contentType(extname); 17 | 18 | return Object.assign(stats, { contentType, date, extname, lastModified, pathname }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/messages.js: -------------------------------------------------------------------------------- 1 | import colors from './colors'; 2 | 3 | const name = 'upsite'; 4 | const play = '→'; 5 | const note = process.platform === 'win32' ? '☼' : '⚡'; 6 | 7 | export default { 8 | isGettingReady: () => console.log(`${colors.yellow(note)} ${name} ${colors.dim('is getting ready')}`), 9 | isInstalling: id => console.log(`${colors.magenta(play)} ${name} ${colors.dim('is installing')} ${id}`), 10 | isNotAvailable: () => console.log(`${colors.red(note)} ${name} ${colors.dim('could not start the server')}`), 11 | isReady: ports => console.log(`${colors.yellow(note)} ${name} ${colors.dim('is ready at')} ${colors.green(asNormalizedPort(ports))}`), 12 | isTrustingHttps: () => console.log(`${colors.magenta(play)} ${name} ${colors.dim('is trusting the https certificates locally')}`) 13 | }; 14 | 15 | function asNormalizedPort(ports) { 16 | return ports.filter(port => port !== 80 && port !== 443).map( 17 | port => `http${port === 80 ? '' : 's'}://localhost${port === 80 || port === 443 ? '' : `:${port}`}/` 18 | ).join(' ') || 'https://localhost/'; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/server-get-first-available-port.js: -------------------------------------------------------------------------------- 1 | import net from 'net'; 2 | 3 | /** 4 | * @function getFirstAvailablePort 5 | * @description return a promise for the first available port for a connection 6 | * @param {Number} port - port for the connection 7 | * @param {...Number} ignorePorts - ports to be ignored for the connection 8 | * @return {Promise} promise for the first available port for a connection 9 | */ 10 | 11 | export default function getFirstAvailablePort(port) { 12 | let portPromise = Promise.reject(); 13 | 14 | for ( 15 | let currentPort = Math.min(Math.max(port, 0), 9999); 16 | currentPort <= 9999; 17 | ++currentPort 18 | ) { 19 | portPromise = portPromise.catch(() => isPortAvailable(currentPort)); 20 | } 21 | 22 | return portPromise; 23 | } 24 | 25 | /** 26 | * @function isPortAvailable 27 | * @description return a promise for whether the port is available for a connection 28 | * @param {Number} port - port for the connection 29 | * @return {Promise} promise for whether the port is availale for a connection 30 | */ 31 | 32 | function isPortAvailable(port) { 33 | return new Promise((resolve, reject) => { 34 | const tester = net.createServer().once('error', reject).once('listening', () => { 35 | tester.once('close', () => resolve(port)).close(); 36 | }).listen(port); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import getFirstAvailablePort from './server-get-first-available-port'; 4 | 5 | /** 6 | * @name Server 7 | * @class 8 | * @extends https.Server 9 | * @classdesc Creates a new HTTP & HTTPS Server. 10 | * @param {Object} options - options for the server 11 | * @param {Function} options.listener - listener for the server 12 | * @param {Number} options.port - primary port for the connection 13 | * @param {String} options.cert - self-signed certificate 14 | * @param {String} options.key - private key of the certificate 15 | * @return {Server} 16 | */ 17 | 18 | export default class Server extends https.Server { 19 | constructor(options, connectionListener) { 20 | super(Object.assign({ allowHTTP1: true }, options), connectionListener); 21 | 22 | this._tlsHandler = typeof this._events.connection === 'function' 23 | ? this._events.connection 24 | : this._tlsHandler = this._events.connection[this._events.connection.length - 1]; 25 | 26 | this.removeListener('connection', this._tlsHandler); 27 | 28 | this.on('connection', onConnection); 29 | 30 | this.timeout = 2 * 60 * 1000; 31 | this.allowHalfOpen = true; 32 | this.httpAllowHalfOpen = false; 33 | } 34 | 35 | setTimeout(msecs, callback) { 36 | this.timeout = msecs; 37 | 38 | if (callback) { 39 | this.on('timeout', callback); 40 | } 41 | } 42 | 43 | listen(ports) { 44 | return [].concat(ports || []).reduce( 45 | (promise, port) => promise.then( 46 | servers => getFirstAvailablePort(port).then(availablePort => { 47 | const server = Object.create(this); 48 | 49 | server.port = availablePort; 50 | 51 | https.Server.prototype.listen.call(server, server.port); 52 | 53 | return servers.concat(server); 54 | }) 55 | ), 56 | Promise.resolve([]) 57 | ); 58 | } 59 | } 60 | 61 | function onError() {} 62 | 63 | function onConnection(socket) { 64 | const data = socket.read(1); 65 | 66 | let hasReceivedData = false; 67 | 68 | if (data === null) { 69 | socket.removeListener('error', onError); 70 | socket.on('error', onError); 71 | 72 | socket.once('readable', () => { 73 | onConnection.call(this, socket); 74 | }); 75 | 76 | if (!hasReceivedData) { 77 | socket.removeListener('end', killSocket); 78 | socket.on('end', killSocket); 79 | } 80 | } else { 81 | hasReceivedData = true; 82 | 83 | socket.removeListener('error', onError); 84 | 85 | const firstByte = data[0]; 86 | 87 | socket.unshift(data); 88 | 89 | if (firstByte < 32 || firstByte >= 127) { 90 | this._tlsHandler(socket); 91 | } else { 92 | http._connectionListener.call(this, socket); 93 | } 94 | } 95 | } 96 | 97 | function killSocket() { 98 | if (this.writable) { 99 | this.end(); 100 | } 101 | } 102 | 103 | /** 104 | * @external http.Server 105 | * @see https://nodejs.org/api/http.html#http_class_http_server 106 | * @external https.Server 107 | * @see https://nodejs.org/api/https.html#https_class_https_server 108 | * @external tls.Server 109 | * @see https://nodejs.org/api/tls.html#tls_class_tls_server 110 | */ 111 | -------------------------------------------------------------------------------- /src/lib/touch-file-as.js: -------------------------------------------------------------------------------- 1 | import fse from 'fse'; 2 | 3 | export default function touchFileAs(filename, fallbackData) { 4 | return fse.open(filename, 'r').catch(error => { 5 | if (error.code === 'ENOENT') { 6 | return fse.writeFile(filename, typeof fallbackData === 'function' ? fallbackData() : fallbackData); 7 | } 8 | 9 | throw error; 10 | }); 11 | } 12 | 13 | export function touchPackageJson() { 14 | return touchFileAs('package.json', JSON.stringify({ private: true }, null, ' ')); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | import yamlLoader from 'js-yaml/lib/js-yaml/loader'; 2 | import child_process from 'child_process'; 3 | import fs from 'fs'; 4 | import parseAsJS from 'require-from-string'; 5 | import path from 'path'; 6 | 7 | const { safeLoad: parseAsYaml } = yamlLoader 8 | const { parse: parseAsJSON } = JSON; 9 | export const cwd = process.cwd(); 10 | const initialOpts = { 11 | cwd, 12 | entry: 'main', 13 | index: ['index.js', 'index.json'], 14 | npmInstallOptions: '--no-save', 15 | ext: ['js'] 16 | }; 17 | 18 | /* Require a module 19 | /* ========================================================================== */ 20 | 21 | export function required (id, rawopts, rawcache) { 22 | const opts = { ...initialOpts, ...Object(rawopts) }; 23 | const cache = Object(rawcache); 24 | 25 | return resolve_w_install(id, opts, cache).then(require_from_result); 26 | } 27 | 28 | export function require_config (id, rawopts, rawcache) { 29 | const opts = { ...initialOpts, ...Object(rawopts) }; 30 | const cache = Object(rawcache); 31 | 32 | return get_cached_json_contents(opts.cwd, cache).then( 33 | // if `pkg` has the entry field 34 | pkg => id in pkg 35 | // resolve `pkg/id` as the config 36 | ? Object(pkg[id]) 37 | // otherwise, reject the package.json 38 | : Promise.reject() 39 | ).catch(() => [ 40 | `.${id}rc`, 41 | `.${id}rc.json`, 42 | `.${id}rc.yaml`, 43 | `.${id}rc.yml`, 44 | `.${id}rc.js`, 45 | `${id}.config.js` 46 | ].reduce( 47 | (promise, file) => promise.catch( 48 | () => resolve_as_file(file, cache).then(require_from_result), 49 | ), 50 | Promise.reject() 51 | )).catch(() => null); 52 | } 53 | 54 | /* Resolve the location of a module 55 | /* ========================================================================== */ 56 | 57 | function resolve_w_install (id, rawopts, rawcache) { 58 | const opts = { ...initialOpts, ...Object(rawopts) }; 59 | const cache = Object(rawcache); 60 | 61 | return resolve(id, opts, cache).catch(error => { 62 | if (!starts_with_relative(id)) { 63 | const cmd = `npm install ${opts.npmInstallOptions} ${id}`; 64 | 65 | if (typeof opts.onBeforeNpmInstall === 'function') { 66 | opts.onBeforeNpmInstall(id); 67 | } 68 | 69 | return exec(cmd, opts).then( 70 | () => { 71 | if (typeof opts.onAfterNpmInstall === 'function') { 72 | opts.onAfterNpmInstall(id); 73 | } 74 | 75 | return resolve_as_module(id, opts, cache); 76 | } 77 | ) 78 | } 79 | 80 | throw error; 81 | }) 82 | } 83 | 84 | function resolve (id, rawopts, rawcache) { 85 | const opts = { ...initialOpts, ...Object(rawopts) }; 86 | const cache = Object(rawcache); 87 | 88 | // if `id` starts with `/` then `cwd` is the filesystem root 89 | opts.cwd = starts_with_root(id) ? '' : opts.cwd; 90 | 91 | return ( 92 | // if `id` begins with `/`, `./`, or `../` 93 | starts_with_relative(id) 94 | // resolve as a path using `cwd/id` as `file` 95 | ? resolve_as_path(path.join(opts.cwd, id), opts, cache) 96 | // otherwise, resolve as a module using `cwd` and `id` 97 | : resolve_as_module(id, opts, cache) 98 | // otherwise, throw "Module id could not be loaded" 99 | ).catch( 100 | error => Promise.reject(Object.assign(new Error(`${id} could not be loaded`), error)) 101 | ); 102 | } 103 | 104 | /* Related tooling 105 | /* ========================================================================== */ 106 | 107 | function resolve_as_path (id, opts, cache) { 108 | // resolve as a path using `cwd/id` as `file` 109 | return resolve_as_file(id, cache) 110 | // otherwise, resolve as a directory using `dir/id` as `dir` 111 | .catch(() => resolve_as_directory(id, opts, cache)) 112 | } 113 | 114 | function resolve_as_file (file, cache) { 115 | return new Promise((resolvePromise, rejectPromise) => { 116 | fs.stat( 117 | file, 118 | (error, stats) => error 119 | ? rejectPromise(error) 120 | : cache[file] && cache[file].mtimeMs === stats.mtimeMs 121 | ? resolvePromise(cache[file]) 122 | : resolvePromise(get_cached_file_contents(file, stats.mtimeMs, cache)) 123 | ) 124 | }); 125 | } 126 | 127 | function resolve_as_directory (dir, opts, cache) { 128 | // resolve the JSON contents of `dir/package.json` as `pkg` 129 | return get_cached_json_contents(dir, cache).then( 130 | // if `pkg` has the entry field 131 | pkg => 'entry' in opts && opts.entry in pkg 132 | // resolve `dir/entry` as the file 133 | ? resolve_as_path(path.join(dir, pkg[opts.entry]), opts, cache) 134 | // otherwise, resolve `dir/index` as the file 135 | : Promise.reject() 136 | ).catch(error => { 137 | return opts.index.reduce( 138 | (promise, index) => promise.catch( 139 | () => resolve_as_file(path.join(dir, index), cache) 140 | ), 141 | Promise.reject() 142 | ).catch(() => opts.ext.reduce( 143 | (promise, ext) => promise.catch( 144 | () => resolve_as_file(dir + '.' + ext, cache) 145 | ), 146 | Promise.reject(error) 147 | )) 148 | }) 149 | } 150 | 151 | function resolve_as_module (id, opts, cache) { 152 | // for each `dir` in the node modules directory using `cwd` 153 | return get_node_modules_dirs(opts.cwd).reduce( 154 | (promise, dir) => promise.catch( 155 | // resolve as a file using `dir/id` as `file` 156 | () => resolve_as_file(path.join(dir, id), cache) 157 | // otherwise, resolve as a directory using `dir/id` as `dir` 158 | .catch(() => resolve_as_directory(path.join(dir, id), opts, cache)) 159 | ), 160 | Promise.reject() 161 | ); 162 | } 163 | 164 | function require_as_js_from_result (result) { 165 | return parseAsJS(result.contents, result.file); 166 | } 167 | 168 | function require_as_json_from_result (result) { 169 | return parseAsJSON(result.contents); 170 | } 171 | 172 | function require_as_yaml_from_result (result) { 173 | return parseAsYaml(result.contents, { filename: result.file }); 174 | } 175 | 176 | function require_as_any_from_result (result) { 177 | try { 178 | return require_as_json_from_result(result); 179 | } catch (error1) { 180 | try { 181 | return require_as_yaml_from_result(result); 182 | } catch (error2) { 183 | try { 184 | return require_as_js_from_result(result); 185 | } catch (error3) { 186 | return result.contents; 187 | } 188 | } 189 | } 190 | } 191 | 192 | /* Supporting function 193 | /* ========================================================================== */ 194 | 195 | function require_from_result (result) { 196 | const extname = path.extname(result.file).slice(1).toLowerCase(); 197 | 198 | const requiredExport = extname === 'es6' || extname === 'js' || extname === 'mjs' 199 | ? require_as_js_from_result(result) 200 | : extname === 'json' 201 | ? require_as_json_from_result(result) 202 | : extname === 'yaml' || extname === 'yml' 203 | ? require_as_yaml_from_result(result) 204 | : require_as_any_from_result(result); 205 | 206 | return requiredExport; 207 | } 208 | 209 | function get_node_modules_dirs (dir) { 210 | // segments is `dir` split by `/` 211 | const segments = dir.split(path.sep); 212 | 213 | // `count` is the length of segments 214 | let count = segments.length; 215 | 216 | // `dirs` is an empty list 217 | const dirs = []; 218 | 219 | // while `count` is greater than `0` 220 | while (count > 0) { 221 | // if `segments[count]` is not `node_modules` 222 | if (segments[count] !== 'node_modules') { 223 | // push a new path to `dirs` as the `/`-joined `segments[0 - count]` and `node_modules` 224 | dirs.push( 225 | path.join(segments.slice(0, count).join('/') || '/', 'node_modules') 226 | ); 227 | } 228 | 229 | // `count` is `count` minus `1` 230 | --count; 231 | } 232 | 233 | // return `dirs` 234 | return dirs; 235 | } 236 | 237 | function get_cached_json_contents (dir, cache) { 238 | const file = path.join(dir, 'package.json'); 239 | 240 | return resolve_as_file(file, cache).then( 241 | ({ contents }) => parseAsJSON(contents) 242 | ); 243 | } 244 | 245 | function get_cached_file_contents (file, mtimeMs, cache) { 246 | cache[file] = new Promise( 247 | (resolvePromise, rejectPromise) => fs.readFile( 248 | file, 249 | 'utf8', 250 | (error, contents) => error 251 | ? rejectPromise(error) 252 | : resolvePromise({ file, contents }) 253 | ) 254 | ); 255 | 256 | cache[file].mtimeMs = mtimeMs; 257 | 258 | return cache[file]; 259 | } 260 | 261 | function starts_with_root (id) { 262 | return /^\//.test(id); 263 | } 264 | 265 | function starts_with_relative (id) { 266 | return /^\.{0,2}\//.test(id); 267 | } 268 | 269 | export function exec(cmd, rawopts) { 270 | const opts = { ...initialOpts, ...Object(rawopts) }; 271 | 272 | return new Promise((resolvePromise, rejectPromise) => { 273 | child_process.exec(cmd, { 274 | stdio: [null, null, null], 275 | cwd: opts.cwd 276 | }, (error, stdout) => { 277 | if (error) { 278 | rejectPromise(stdout); 279 | } else { 280 | resolvePromise(stdout); 281 | } 282 | }); 283 | }); 284 | } 285 | --------------------------------------------------------------------------------