UNPKG

@hqjs/hq

Version:

Universal static web server for frontend development

440 lines (404 loc) 12.6 kB
import fg from 'fast-glob'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; import resolvePackage from 'resolve'; import url from 'url'; const MAX_RETRY = 30; const packageJSONMap = new Map; const getLocalIP = () => { const interfaces = os.networkInterfaces(); for (const intfs of Object.values(interfaces)) { for (const intf of intfs) { if (intf.family === 'IPv4' && !intf.internal) return intf; } } return null; }; const { address: LOCAL_IP } = getLocalIP(); const WORKER_REXP = /(worker|sw)\d*\b/i; export const HTTP_CODES = { INTERNAL_SERVER_ERROR: 500, NOT_ACCEPTABLE: 406, NOT_FOUND: 404, NOT_MODIFIED: 304, OK: 200, }; export const WATCH_EXTENSIONS = [ 'pug', 'html', 'css', 'scss', 'sass', 'less', 'js', 'jsx', 'es6', 'mjs', 'vue', 'svelte', 'json', 'ts', 'tsx', 'coffee', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'glsl', 'vert', 'frag', ]; export const pathToURL = filePath => url.pathToFileURL(filePath).href.slice('file://'.length).replace(/^[a-zA-Z]:/, ''); export const urlToPath = urlPath => path.sep === '\\' ? urlPath.replace(/\//g, '\\') : urlPath; export const getVersion = (dependencies, name) => { if (!dependencies) return {}; const version = dependencies[name]; if (!version) return {}; const [ major, minor, patch ] = version.match(/\d+/g); return { major: Number(major), minor: Number(minor), patch: Number(patch), }; }; const matchesModule = (filePath, module) => filePath === `/node_modules/${module}` || filePath.startsWith(`/node_modules/${module}/`); export const isMap = filePath => path.extname(filePath).toLowerCase() === '.map'; export const isTest = filePath => filePath.startsWith('/test/'); export const isVendor = filePath => filePath.startsWith('/node_modules/'); export const isPolyfill = filePath => matchesModule(filePath, 'core-js') || matchesModule(filePath, 'buffer') || matchesModule(filePath, 'base64-js') || matchesModule(filePath, 'ieee754') || matchesModule(filePath, 'process') || matchesModule(filePath, 'regenerator-runtime'); export const isInternal = filePath => filePath.includes('/hq-livereload.js') || filePath.includes('/hq-empty-module.js'); export const isCertificate = (filePath, app) => app.certs.includes(filePath); export const isWorker = filePath => WORKER_REXP.test(filePath); export const isDefaultFavicon = filePath => filePath.endsWith('favicon.ico'); export const isAngularCompiler = filePath => filePath.endsWith('compiler/fesm5/compiler.js'); export const isSource = ext => [ '.pug', '.html', '.css', '.scss', '.sass', '.less', '.js', '.jsx', '.mjs', '.es6', '.vue', '.svelte', '.ts', '.tsx', '.coffee', '.map', ].includes(ext); export const getResType = ext => { switch (ext) { case '.jsx': case '.ts': case '.tsx': case '.es6': case '.vue': case '.svelte': case '.coffee': return '.js'; case '.scss': case '.sass': case '.less': return '.css'; case '.pug': return '.html'; default: return ext; } }; /* eslint-disable complexity */ // TODO: delete this method it is unused export const getLinkType = (ext, name) => { // TODO add other types https://w3c.github.io/preload/#as-attribute switch (ext) { case '.js': case '.jsx': case '.es6': case '.vue': case '.svelte': case '.ts': case '.tsx': case '.coffee': case '.mjs': return WORKER_REXP.test(name) ? 'worker' : 'script'; case '.json': return 'script'; case '.scss': case '.sass': case '.less': case '.css': return 'style'; case '.pug': case '.html': return 'document'; case '.woff': case '.woff2': return 'font'; case '.gif': case '.png': case '.jpg': case '.jpeg': case '.svg': case '.webp': return 'image'; default: return ''; } }; /* eslint-enable complexity */ export const findExistingExtension = async filepath => { if (filepath.endsWith('index') && await fs.pathExists(`${filepath}.html`)) return '.html'; else if (await fs.pathExists(`${filepath}.jsx`)) return '.jsx'; else if (await fs.pathExists(`${filepath}.vue`)) return '.vue'; else if (await fs.pathExists(`${filepath}.svelte`)) return '.svelte'; else if (await fs.pathExists(`${filepath}.mjs`)) return '.mjs'; else if (await fs.pathExists(`${filepath}.json`)) return '.json'; else if (await fs.pathExists(`${filepath}.ts`)) return '.ts'; else if (await fs.pathExists(`${filepath}.tsx`)) return '.tsx'; else if (await fs.pathExists(`${filepath}.coffee`)) return '.coffee'; else if (await fs.pathExists(`${filepath}.es6`)) return '.es6'; else if (await fs.pathExists(`${filepath}.js`)) return '.js'; else if (await fs.pathExists(filepath)) return ''; else if (!filepath.endsWith('index') && await fs.pathExists(`${filepath}.html`)) return '.html'; else throw new Error(`File ${filepath} not found`); }; export const getModulePath = filepath => { const parts = pathToURL(filepath).split('/node_modules/'); return `/node_modules/${parts[parts.length - 1]}`; }; export const getPackageJSONDir = async dir => { let dirPath = dir; let prev = ''; while (dirPath !== prev && !await fs.pathExists(path.join(dirPath, 'package.json'))) { prev = dirPath; dirPath = path.join(dirPath, '..'); } if (!await fs.pathExists(path.join(dirPath, 'package.json'))) return null; return dirPath; }; export const readPackageJSON = async ( dir, { search = true } = {}, fields = [ 'browser', 'exports', 'main', 'module', 'version' ], ) => { const dirPath = search ? await getPackageJSONDir(dir) : dir; if (packageJSONMap.has(dirPath)) return packageJSONMap.get(dirPath); try { const packageJSON = JSON.parse(await fs.readFile(path.join(dirPath, 'package.json'), { encoding: 'utf8' })); const filteredJSON = {}; for (const field of fields) { filteredJSON[field] = packageJSON[field]; } packageJSONMap.set(dirPath, filteredJSON); return filteredJSON; } catch { return {}; } }; export const resolvePackageMain = async (dir, { search = false } = {}) => { const dirPath = search ? await getPackageJSONDir(dir) : dir; const packageJSON = await readPackageJSON(dirPath, { search: false }); return ( typeof packageJSON.browser === 'string' && packageJSON.browser) || ( typeof packageJSON.browser === 'object' && packageJSON.browser && packageJSON.main && (packageJSON.browser[`./${packageJSON.main}`] || packageJSON.browser[packageJSON.main]) ) || packageJSON.module || packageJSON.exports || packageJSON.main || `index${await findExistingExtension(`${dirPath}/index`)}`; }; const resolveOrModify = (pkgPath, pkg, { emptyPath, resolve, result }) => { const pkgBasename = pkgPath.slice(0, -path.extname(pkgPath).length); if (typeof pkg.browser[pkgPath] === 'string') { result.modified = true; pkg.main = pkg.browser[pkgPath]; } else if (typeof pkg.browser[pkgPath] === 'boolean') { result.resolved = true; resolve(emptyPath); } else if (typeof pkg.browser[`./${pkgPath}`] === 'string') { result.modified = true; pkg.main = pkg.browser[`./${pkgPath}`]; } else if (typeof pkg.browser[`./${pkgPath}`] === 'boolean') { result.resolved = true; resolve(emptyPath); } else if (typeof pkg.browser[`./${pkgPath}.js`] === 'string') { result.modified = true; pkg.main = pkg.browser[`./${pkgPath}.js`]; } else if (typeof pkg.browser[`./${pkgPath}.js`] === 'boolean') { result.resolved = true; resolve(emptyPath); } else if (typeof pkg.browser[`./${pkgBasename}.js`] === 'string') { result.modified = true; pkg.main = pkg.browser[`./${pkgBasename}.js`]; } else if (typeof pkg.browser[`./${pkgBasename}.js`] === 'boolean') { result.resolved = true; resolve(emptyPath); } }; export const resolvePackageFrom = (basedir, dpath, hqroot) => new Promise((resolve, reject) => { const emptyPath = path.resolve(hqroot, 'hq-empty-module.js'); const parts = dpath.split('/node_modules/'); const modName = parts[parts.length - 1]; const modPath = modName .split('/') .slice(1) .join('/'); const modResolve = resolvePackage.isCore(modName) ? `${modName}/` : modName; const result = { modified: false, resolved: false, }; return resolvePackage( modResolve, { basedir, extensions: [ '.js', '.jsx', '.mjs', '.es6', '.vue', '.svelte', '.ts', '.tsx', '.coffee', '.css', '.scss', '.sass', '.less', '.pug', '.html', ], packageFilter(pkg) { const { main: pkgMain } = pkg; if (pkg.module) pkg.main = pkg.module; if (typeof pkg.browser === 'string') pkg.main = pkg.browser; else if (typeof pkg.browser === 'object' && pkg.browser) { if (modPath) { resolveOrModify(modPath, pkg, { emptyPath, resolve, result }); } else if (pkgMain) { resolveOrModify(pkgMain, pkg, { emptyPath, resolve, result }); } else if (pkg.module) { resolveOrModify(pkg.module, pkg, { emptyPath, resolve, result }); } } return pkg; }, pathFilter(pkg, fullPath, relativePath) { return result.modified ? pkg.main : relativePath; }, }, (err, p) => { if (result.resolved) return; if (err) reject(err); resolve(p); }, ); }); export const readPlugins = async (app, config) => { try { const { plugins } = JSON.parse(await fs.readFile(config, { encoding: 'utf-8' })); const pluginsConfig = await Promise.all(plugins.map(async p => { const [ pluginName, ...args ] = Array.isArray(p) ? p : [ p ]; const pluginPath = await resolvePackageFrom(app.root, `/node_modules/${pluginName}`, app.hqroot); const { default: plugin } = await import(pluginPath); return { args, plugin }; })); return pluginsConfig.map(({ args, plugin }) => plugin(...args)); } catch { return []; } }; /* eslint-disable no-unused-expressions */ const getFreeServer = ({ app, certs, cfg, host, net, port, retry, root, s, secure, }) => new Promise((resolve, reject) => { const server = secure ? net.createSecureServer({ allowHTTP1: true, ...cfg }, app.callback()) : net.createServer(app.callback()); server.unref(); server.on('error', reject); // Next 2 lines required for vscode plugin server.localIP = LOCAL_IP; server.protocol = `http${s}`; server.listen(port, host, () => { if (!app.build) { console.log(`Start time: ${process.uptime().toFixed(1)} s`); console.log(`Visit http${s}://localhost:${port}\nor http${s}://${LOCAL_IP}:${port} within local network`); } else { console.log('Building...'); } import('./compilers/html.mjs'); resolve({ certs: certs.map(crt => crt.slice(root.length)), server, }); }); }).catch(err => { if (retry > MAX_RETRY) throw err; return getFreeServer({ app, certs, cfg, host, net, port: port + 1, retry: retry + 1, root, s, secure }); }); /* eslint-enable no-unused-expressions */ export const getServer = async ({ app, host, port, root }) => { const certs = await fg(`${root}/**/*.pem`, { ignore: [ `${root}/node_modules/**` ] }); const cfg = (await Promise.all(certs.slice(0, 2).map(crt => fs.readFile(crt)))) .reduce( ({ cert, key }, file, index) => certs[index].endsWith('key.pem') ? { cert, key: file } : { cert: file, key }, { cert: null, key: null }, ); const secure = Boolean(cfg.cert && cfg.key); const s = secure ? 's' : ''; const net = await (secure ? import('http2') : import('http') ); return getFreeServer({ app, certs, cfg, host, net, port, retry: 0, root, s, secure, }); }; export const getSrc = async root => { const [ packageJSON, rootHTML, srcHTML, srcExists ] = await Promise.all([ readPackageJSON(root), fs.pathExists(path.join(root, './index.html')), fs.pathExists(path.join(root, 'src/index.html')), fs.pathExists(path.join(root, 'src')), ]); return packageJSON.module ? path.dirname(packageJSON.module) : packageJSON.exports ? path.dirname(packageJSON.exports) : srcHTML ? 'src' : rootHTML ? '.' : srcExists ? 'src' : packageJSON.main ? path.dirname(packageJSON.main) : '.'; };