mirror of
https://github.com/coder/code-server.git
synced 2024-12-04 23:03:06 +08:00
eae5d8c807
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
645 lines
20 KiB
JavaScript
645 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
// @ts-check
|
|
|
|
const http = require('http');
|
|
const url = require('url');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const util = require('util');
|
|
const opn = require('opn');
|
|
const minimist = require('minimist');
|
|
const fancyLog = require('fancy-log');
|
|
const ansiColors = require('ansi-colors');
|
|
const remote = require('gulp-remote-retry-src');
|
|
const vfs = require('vinyl-fs');
|
|
const uuid = require('uuid');
|
|
|
|
const extensions = require('../../build/lib/extensions');
|
|
const { getBuiltInExtensions } = require('../../build/lib/builtInExtensions');
|
|
|
|
const APP_ROOT = path.join(__dirname, '..', '..');
|
|
const BUILTIN_EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions');
|
|
const BUILTIN_MARKETPLACE_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInExtensions');
|
|
const WEB_DEV_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInWebDevExtensions');
|
|
const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html');
|
|
|
|
// This is useful to simulate real world CORS
|
|
const ALLOWED_CORS_ORIGINS = [
|
|
'http://localhost:8081',
|
|
'http://127.0.0.1:8081',
|
|
'http://localhost:8080',
|
|
'http://127.0.0.1:8080',
|
|
];
|
|
|
|
const WEB_PLAYGROUND_VERSION = '0.0.10';
|
|
|
|
const args = minimist(process.argv, {
|
|
boolean: [
|
|
'no-launch',
|
|
'help',
|
|
'verbose',
|
|
'wrap-iframe',
|
|
'enable-sync',
|
|
],
|
|
string: [
|
|
'scheme',
|
|
'host',
|
|
'port',
|
|
'local_port',
|
|
'extension',
|
|
'github-auth'
|
|
],
|
|
});
|
|
|
|
if (args.help) {
|
|
console.log(
|
|
'yarn web [options]\n' +
|
|
' --no-launch Do not open VSCode web in the browser\n' +
|
|
' --wrap-iframe Wrap the Web Worker Extension Host in an iframe\n' +
|
|
' --enable-sync Enable sync by default\n' +
|
|
' --scheme Protocol (https or http)\n' +
|
|
' --host Remote host\n' +
|
|
' --port Remote/Local port\n' +
|
|
' --local_port Local port override\n' +
|
|
' --secondary-port Secondary port\n' +
|
|
' --extension Path of an extension to include\n' +
|
|
' --github-auth Github authentication token\n' +
|
|
' --verbose Print out more information\n' +
|
|
' --help\n' +
|
|
'[Example]\n' +
|
|
' yarn web --scheme https --host example.com --port 8080 --local_port 30000'
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const PORT = args.port || process.env.PORT || 8080;
|
|
const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT;
|
|
const SECONDARY_PORT = args['secondary-port'] || (parseInt(PORT, 10) + 1);
|
|
const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http';
|
|
const HOST = args.host || 'localhost';
|
|
const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`;
|
|
|
|
const exists = (path) => util.promisify(fs.exists)(path);
|
|
const readFile = (path) => util.promisify(fs.readFile)(path);
|
|
|
|
async function getBuiltInExtensionInfos() {
|
|
await getBuiltInExtensions();
|
|
|
|
const allExtensions = [];
|
|
/** @type {Object.<string, string>} */
|
|
const locations = {};
|
|
|
|
const [localExtensions, marketplaceExtensions, webDevExtensions] = await Promise.all([
|
|
extensions.scanBuiltinExtensions(BUILTIN_EXTENSIONS_ROOT),
|
|
extensions.scanBuiltinExtensions(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT),
|
|
ensureWebDevExtensions().then(() => extensions.scanBuiltinExtensions(WEB_DEV_EXTENSIONS_ROOT))
|
|
]);
|
|
for (const ext of localExtensions) {
|
|
allExtensions.push(ext);
|
|
locations[ext.extensionPath] = path.join(BUILTIN_EXTENSIONS_ROOT, ext.extensionPath);
|
|
}
|
|
for (const ext of marketplaceExtensions) {
|
|
allExtensions.push(ext);
|
|
locations[ext.extensionPath] = path.join(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT, ext.extensionPath);
|
|
}
|
|
for (const ext of webDevExtensions) {
|
|
allExtensions.push(ext);
|
|
locations[ext.extensionPath] = path.join(WEB_DEV_EXTENSIONS_ROOT, ext.extensionPath);
|
|
}
|
|
for (const ext of allExtensions) {
|
|
if (ext.packageJSON.browser) {
|
|
let mainFilePath = path.join(locations[ext.extensionPath], ext.packageJSON.browser);
|
|
if (path.extname(mainFilePath) !== '.js') {
|
|
mainFilePath += '.js';
|
|
}
|
|
if (!await exists(mainFilePath)) {
|
|
fancyLog(`${ansiColors.red('Error')}: Could not find ${mainFilePath}. Use ${ansiColors.cyan('yarn watch-web')} to build the built-in extensions.`);
|
|
}
|
|
}
|
|
}
|
|
return { extensions: allExtensions, locations };
|
|
}
|
|
|
|
async function ensureWebDevExtensions() {
|
|
|
|
// Playground (https://github.com/microsoft/vscode-web-playground)
|
|
const webDevPlaygroundRoot = path.join(WEB_DEV_EXTENSIONS_ROOT, 'vscode-web-playground');
|
|
const webDevPlaygroundExists = await exists(webDevPlaygroundRoot);
|
|
|
|
let downloadPlayground = false;
|
|
if (webDevPlaygroundExists) {
|
|
try {
|
|
const webDevPlaygroundPackageJson = JSON.parse(((await readFile(path.join(webDevPlaygroundRoot, 'package.json'))).toString()));
|
|
if (webDevPlaygroundPackageJson.version !== WEB_PLAYGROUND_VERSION) {
|
|
downloadPlayground = true;
|
|
}
|
|
} catch (error) {
|
|
downloadPlayground = true;
|
|
}
|
|
} else {
|
|
downloadPlayground = true;
|
|
}
|
|
|
|
if (downloadPlayground) {
|
|
if (args.verbose) {
|
|
fancyLog(`${ansiColors.magenta('Web Development extensions')}: Downloading vscode-web-playground to ${webDevPlaygroundRoot}`);
|
|
}
|
|
await new Promise((resolve, reject) => {
|
|
remote(['package.json', 'dist/extension.js', 'dist/extension.js.map'], {
|
|
base: 'https://raw.githubusercontent.com/microsoft/vscode-web-playground/main/'
|
|
}).pipe(vfs.dest(webDevPlaygroundRoot)).on('end', resolve).on('error', reject);
|
|
});
|
|
} else {
|
|
if (args.verbose) {
|
|
fancyLog(`${ansiColors.magenta('Web Development extensions')}: Using existing vscode-web-playground in ${webDevPlaygroundRoot}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getCommandlineProvidedExtensionInfos() {
|
|
const extensions = [];
|
|
|
|
/** @type {Object.<string, string>} */
|
|
const locations = {};
|
|
|
|
let extensionArg = args['extension'];
|
|
if (!extensionArg) {
|
|
return { extensions, locations };
|
|
}
|
|
|
|
const extensionPaths = Array.isArray(extensionArg) ? extensionArg : [extensionArg];
|
|
await Promise.all(extensionPaths.map(async extensionPath => {
|
|
extensionPath = path.resolve(process.cwd(), extensionPath);
|
|
const packageJSON = await getExtensionPackageJSON(extensionPath);
|
|
if (packageJSON) {
|
|
const extensionId = `${packageJSON.publisher}.${packageJSON.name}`;
|
|
extensions.push({
|
|
packageJSON,
|
|
extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/extension/${extensionId}` }
|
|
});
|
|
locations[extensionId] = extensionPath;
|
|
}
|
|
}));
|
|
return { extensions, locations };
|
|
}
|
|
|
|
async function getExtensionPackageJSON(extensionPath) {
|
|
|
|
const packageJSONPath = path.join(extensionPath, 'package.json');
|
|
if (await exists(packageJSONPath)) {
|
|
try {
|
|
let packageJSON = JSON.parse((await readFile(packageJSONPath)).toString());
|
|
if (packageJSON.main && !packageJSON.browser) {
|
|
return; // unsupported
|
|
}
|
|
|
|
const packageNLSPath = path.join(extensionPath, 'package.nls.json');
|
|
const packageNLSExists = await exists(packageNLSPath);
|
|
if (packageNLSExists) {
|
|
packageJSON = extensions.translatePackageJSON(packageJSON, packageNLSPath); // temporary, until fixed in core
|
|
}
|
|
|
|
return packageJSON;
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const builtInExtensionsPromise = getBuiltInExtensionInfos();
|
|
const commandlineProvidedExtensionsPromise = getCommandlineProvidedExtensionInfos();
|
|
|
|
const mapCallbackUriToRequestId = new Map();
|
|
|
|
/**
|
|
* @param req {http.IncomingMessage}
|
|
* @param res {http.ServerResponse}
|
|
*/
|
|
const requestHandler = (req, res) => {
|
|
const parsedUrl = url.parse(req.url, true);
|
|
const pathname = parsedUrl.pathname;
|
|
|
|
try {
|
|
if (pathname === '/favicon.ico') {
|
|
// favicon
|
|
return serveFile(req, res, path.join(APP_ROOT, 'resources', 'win32', 'code.ico'));
|
|
}
|
|
if (pathname === '/manifest.json') {
|
|
// manifest
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
return res.end(JSON.stringify({
|
|
'name': 'Code Web - OSS',
|
|
'short_name': 'Code Web - OSS',
|
|
'start_url': '/',
|
|
'lang': 'en-US',
|
|
'display': 'standalone'
|
|
}));
|
|
}
|
|
if (/^\/static\//.test(pathname)) {
|
|
// static requests
|
|
return handleStatic(req, res, parsedUrl);
|
|
}
|
|
if (/^\/extension\//.test(pathname)) {
|
|
// default extension requests
|
|
return handleExtension(req, res, parsedUrl);
|
|
}
|
|
if (pathname === '/') {
|
|
// main web
|
|
return handleRoot(req, res);
|
|
} else if (pathname === '/callback') {
|
|
// callback support
|
|
return handleCallback(req, res, parsedUrl);
|
|
} else if (pathname === '/fetch-callback') {
|
|
// callback fetch support
|
|
return handleFetchCallback(req, res, parsedUrl);
|
|
}
|
|
|
|
return serveError(req, res, 404, 'Not found.');
|
|
} catch (error) {
|
|
console.error(error.toString());
|
|
|
|
return serveError(req, res, 500, 'Internal Server Error.');
|
|
}
|
|
};
|
|
|
|
const server = http.createServer(requestHandler);
|
|
server.listen(LOCAL_PORT, () => {
|
|
if (LOCAL_PORT !== PORT) {
|
|
console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`);
|
|
}
|
|
console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`);
|
|
});
|
|
server.on('error', err => {
|
|
console.error(`Error occurred in server:`);
|
|
console.error(err);
|
|
});
|
|
|
|
const secondaryServer = http.createServer(requestHandler);
|
|
secondaryServer.listen(SECONDARY_PORT, () => {
|
|
console.log(`Secondary server available at ${SCHEME}://${HOST}:${SECONDARY_PORT}`);
|
|
});
|
|
secondaryServer.on('error', err => {
|
|
console.error(`Error occurred in server:`);
|
|
console.error(err);
|
|
});
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
*/
|
|
function addCORSReplyHeader(req) {
|
|
if (typeof req.headers['origin'] !== 'string') {
|
|
// not a CORS request
|
|
return false;
|
|
}
|
|
return (ALLOWED_CORS_ORIGINS.indexOf(req.headers['origin']) >= 0);
|
|
}
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
*/
|
|
async function handleStatic(req, res, parsedUrl) {
|
|
|
|
if (/^\/static\/extensions\//.test(parsedUrl.pathname)) {
|
|
const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/static/extensions/'.length));
|
|
const filePath = getExtensionFilePath(relativePath, (await builtInExtensionsPromise).locations);
|
|
const responseHeaders = {};
|
|
if (addCORSReplyHeader(req)) {
|
|
responseHeaders['Access-Control-Allow-Origin'] = '*';
|
|
}
|
|
if (!filePath) {
|
|
return serveError(req, res, 400, `Bad request.`, responseHeaders);
|
|
}
|
|
return serveFile(req, res, filePath, responseHeaders);
|
|
}
|
|
|
|
// Strip `/static/` from the path
|
|
const relativeFilePath = path.normalize(decodeURIComponent(parsedUrl.pathname.substr('/static/'.length)));
|
|
|
|
return serveFile(req, res, path.join(APP_ROOT, relativeFilePath));
|
|
}
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
*/
|
|
async function handleExtension(req, res, parsedUrl) {
|
|
// Strip `/extension/` from the path
|
|
const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/extension/'.length));
|
|
const filePath = getExtensionFilePath(relativePath, (await commandlineProvidedExtensionsPromise).locations);
|
|
const responseHeaders = {};
|
|
if (addCORSReplyHeader(req)) {
|
|
responseHeaders['Access-Control-Allow-Origin'] = '*';
|
|
}
|
|
if (!filePath) {
|
|
return serveError(req, res, 400, `Bad request.`, responseHeaders);
|
|
}
|
|
return serveFile(req, res, filePath, responseHeaders);
|
|
}
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
*/
|
|
async function handleRoot(req, res) {
|
|
let folderUri = { scheme: 'memfs', path: `/sample-folder` };
|
|
|
|
const match = req.url && req.url.match(/\?([^#]+)/);
|
|
if (match) {
|
|
const qs = new URLSearchParams(match[1]);
|
|
|
|
let gh = qs.get('gh');
|
|
if (gh) {
|
|
if (gh.startsWith('/')) {
|
|
gh = gh.substr(1);
|
|
}
|
|
|
|
const [owner, repo, ...branch] = gh.split('/', 3);
|
|
const ref = branch.join('/');
|
|
folderUri = { scheme: 'github', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' };
|
|
} else {
|
|
let cs = qs.get('cs');
|
|
if (cs) {
|
|
if (cs.startsWith('/')) {
|
|
cs = cs.substr(1);
|
|
}
|
|
|
|
const [owner, repo, ...branch] = cs.split('/');
|
|
const ref = branch.join('/');
|
|
folderUri = { scheme: 'codespace', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' };
|
|
}
|
|
}
|
|
}
|
|
|
|
const { extensions: builtInExtensions } = await builtInExtensionsPromise;
|
|
const { extensions: staticExtensions, locations: staticLocations } = await commandlineProvidedExtensionsPromise;
|
|
|
|
const dedupedBuiltInExtensions = [];
|
|
for (const builtInExtension of builtInExtensions) {
|
|
const extensionId = `${builtInExtension.packageJSON.publisher}.${builtInExtension.packageJSON.name}`;
|
|
if (staticLocations[extensionId]) {
|
|
fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: Ignoring built-in ${extensionId} because it was overridden via --extension argument`);
|
|
continue;
|
|
}
|
|
|
|
dedupedBuiltInExtensions.push(builtInExtension);
|
|
}
|
|
|
|
if (args.verbose) {
|
|
fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${dedupedBuiltInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`);
|
|
fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`);
|
|
}
|
|
|
|
const secondaryHost = (
|
|
req.headers['host']
|
|
? req.headers['host'].replace(':' + PORT, ':' + SECONDARY_PORT)
|
|
: `${HOST}:${SECONDARY_PORT}`
|
|
);
|
|
const webConfigJSON = {
|
|
folderUri: folderUri,
|
|
staticExtensions,
|
|
settingsSyncOptions: {
|
|
enabled: args['enable-sync']
|
|
},
|
|
webWorkerExtensionHostIframeSrc: `${SCHEME}://${secondaryHost}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html`
|
|
};
|
|
if (args['wrap-iframe']) {
|
|
webConfigJSON._wrapWebWorkerExtHostInIframe = true;
|
|
}
|
|
if (req.headers['x-forwarded-host']) {
|
|
// support for running in codespace => no iframe wrapping
|
|
delete webConfigJSON.webWorkerExtensionHostIframeSrc;
|
|
}
|
|
|
|
const authSessionInfo = args['github-auth'] ? {
|
|
id: uuid.v4(),
|
|
providerId: 'github',
|
|
accessToken: args['github-auth'],
|
|
scopes: [['user:email'], ['repo']]
|
|
} : undefined;
|
|
|
|
const data = (await readFile(WEB_MAIN)).toString()
|
|
.replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => escapeAttribute(JSON.stringify(webConfigJSON))) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied
|
|
.replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(dedupedBuiltInExtensions)))
|
|
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '')
|
|
.replace('{{WEBVIEW_ENDPOINT}}', '');
|
|
|
|
const headers = {
|
|
'Content-Type': 'text/html',
|
|
'Content-Security-Policy': 'require-trusted-types-for \'script\';'
|
|
};
|
|
res.writeHead(200, headers);
|
|
return res.end(data);
|
|
}
|
|
|
|
/**
|
|
* Handle HTTP requests for /callback
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
*/
|
|
async function handleCallback(req, res, parsedUrl) {
|
|
const wellKnownKeys = ['vscode-requestId', 'vscode-scheme', 'vscode-authority', 'vscode-path', 'vscode-query', 'vscode-fragment'];
|
|
const [requestId, vscodeScheme, vscodeAuthority, vscodePath, vscodeQuery, vscodeFragment] = wellKnownKeys.map(key => {
|
|
const value = getFirstQueryValue(parsedUrl, key);
|
|
if (value) {
|
|
return decodeURIComponent(value);
|
|
}
|
|
|
|
return value;
|
|
});
|
|
|
|
if (!requestId) {
|
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
return res.end(`Bad request.`);
|
|
}
|
|
|
|
// merge over additional query values that we got
|
|
let query = vscodeQuery;
|
|
let index = 0;
|
|
getFirstQueryValues(parsedUrl, wellKnownKeys).forEach((value, key) => {
|
|
if (!query) {
|
|
query = '';
|
|
}
|
|
|
|
const prefix = (index++ === 0) ? '' : '&';
|
|
query += `${prefix}${key}=${value}`;
|
|
});
|
|
|
|
|
|
// add to map of known callbacks
|
|
mapCallbackUriToRequestId.set(requestId, JSON.stringify({ scheme: vscodeScheme || 'code-oss', authority: vscodeAuthority, path: vscodePath, query, fragment: vscodeFragment }));
|
|
return serveFile(req, res, path.join(APP_ROOT, 'resources', 'web', 'callback.html'), { 'Content-Type': 'text/html' });
|
|
}
|
|
|
|
/**
|
|
* Handle HTTP requests for /fetch-callback
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
*/
|
|
async function handleFetchCallback(req, res, parsedUrl) {
|
|
const requestId = getFirstQueryValue(parsedUrl, 'vscode-requestId');
|
|
if (!requestId) {
|
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
return res.end(`Bad request.`);
|
|
}
|
|
|
|
const knownCallbackUri = mapCallbackUriToRequestId.get(requestId);
|
|
if (knownCallbackUri) {
|
|
mapCallbackUriToRequestId.delete(requestId);
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/json' });
|
|
return res.end(knownCallbackUri);
|
|
}
|
|
|
|
/**
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
* @param {string} key
|
|
* @returns {string | undefined}
|
|
*/
|
|
function getFirstQueryValue(parsedUrl, key) {
|
|
const result = parsedUrl.query[key];
|
|
return Array.isArray(result) ? result[0] : result;
|
|
}
|
|
|
|
/**
|
|
* @param {import('url').UrlWithParsedQuery} parsedUrl
|
|
* @param {string[] | undefined} ignoreKeys
|
|
* @returns {Map<string, string>}
|
|
*/
|
|
function getFirstQueryValues(parsedUrl, ignoreKeys) {
|
|
const queryValues = new Map();
|
|
|
|
for (const key in parsedUrl.query) {
|
|
if (ignoreKeys && ignoreKeys.indexOf(key) >= 0) {
|
|
continue;
|
|
}
|
|
|
|
const value = getFirstQueryValue(parsedUrl, key);
|
|
if (typeof value === 'string') {
|
|
queryValues.set(key, value);
|
|
}
|
|
}
|
|
|
|
return queryValues;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
*/
|
|
function escapeAttribute(value) {
|
|
return value.replace(/"/g, '"');
|
|
}
|
|
|
|
/**
|
|
* @param {string} relativePath
|
|
* @param {Object.<string, string>} locations
|
|
* @returns {string | undefined}
|
|
*/
|
|
function getExtensionFilePath(relativePath, locations) {
|
|
const firstSlash = relativePath.indexOf('/');
|
|
if (firstSlash === -1) {
|
|
return undefined;
|
|
}
|
|
const extensionId = relativePath.substr(0, firstSlash);
|
|
|
|
const extensionPath = locations[extensionId];
|
|
if (!extensionPath) {
|
|
return undefined;
|
|
}
|
|
return path.join(extensionPath, relativePath.substr(firstSlash + 1));
|
|
}
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {string} errorMessage
|
|
*/
|
|
function serveError(req, res, errorCode, errorMessage, responseHeaders = Object.create(null)) {
|
|
responseHeaders['Content-Type'] = 'text/plain';
|
|
res.writeHead(errorCode, responseHeaders);
|
|
res.end(errorMessage);
|
|
}
|
|
|
|
const textMimeType = {
|
|
'.html': 'text/html',
|
|
'.js': 'text/javascript',
|
|
'.json': 'application/json',
|
|
'.css': 'text/css',
|
|
'.svg': 'image/svg+xml',
|
|
};
|
|
|
|
const mapExtToMediaMimes = {
|
|
'.bmp': 'image/bmp',
|
|
'.gif': 'image/gif',
|
|
'.ico': 'image/x-icon',
|
|
'.jpe': 'image/jpg',
|
|
'.jpeg': 'image/jpg',
|
|
'.jpg': 'image/jpg',
|
|
'.png': 'image/png',
|
|
'.tga': 'image/x-tga',
|
|
'.tif': 'image/tiff',
|
|
'.tiff': 'image/tiff',
|
|
'.woff': 'application/font-woff'
|
|
};
|
|
|
|
/**
|
|
* @param {string} forPath
|
|
*/
|
|
function getMediaMime(forPath) {
|
|
const ext = path.extname(forPath);
|
|
|
|
return mapExtToMediaMimes[ext.toLowerCase()];
|
|
}
|
|
|
|
/**
|
|
* @param {import('http').IncomingMessage} req
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {string} filePath
|
|
*/
|
|
async function serveFile(req, res, filePath, responseHeaders = Object.create(null)) {
|
|
try {
|
|
|
|
// Sanity checks
|
|
filePath = path.normalize(filePath); // ensure no "." and ".."
|
|
|
|
const stat = await util.promisify(fs.stat)(filePath);
|
|
|
|
// Check if file modified since
|
|
const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag)
|
|
if (req.headers['if-none-match'] === etag) {
|
|
res.writeHead(304);
|
|
return res.end();
|
|
}
|
|
|
|
// Headers
|
|
responseHeaders['Content-Type'] = textMimeType[path.extname(filePath)] || getMediaMime(filePath) || 'text/plain';
|
|
responseHeaders['Etag'] = etag;
|
|
|
|
res.writeHead(200, responseHeaders);
|
|
|
|
// Data
|
|
fs.createReadStream(filePath).pipe(res);
|
|
} catch (error) {
|
|
console.error(error.toString());
|
|
responseHeaders['Content-Type'] = 'text/plain';
|
|
res.writeHead(404, responseHeaders);
|
|
return res.end('Not found');
|
|
}
|
|
}
|
|
|
|
if (args.launch !== false) {
|
|
opn(`${SCHEME}://${HOST}:${PORT}`);
|
|
}
|