",
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 |
--------------------------------------------------------------------------------