react-native
Version:
A framework for building native apps using React
994 lines (896 loc) • 30.7 kB
JavaScript
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const AssetServer = require('../AssetServer');
const getPlatformExtension = require('../node-haste').getPlatformExtension;
const Bundler = require('../Bundler');
const MultipartResponse = require('./MultipartResponse');
const declareOpts = require('../lib/declareOpts');
const defaults = require('../../defaults');
const mime = require('mime-types');
const path = require('path');
const symbolicate = require('./symbolicate');
const terminal = require('../lib/terminal');
const url = require('url');
const debug = require('debug')('RNP:Server');
import type Module, {HasteImpl} from '../node-haste/Module';
import type {Stats} from 'fs';
import type {IncomingMessage, ServerResponse} from 'http';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Bundle from '../Bundler/Bundle';
import type {Reporter} from '../lib/reporting';
import type {GetTransformOptions} from '../Bundler';
import type GlobalTransformCache from '../lib/GlobalTransformCache';
import type {SourceMap, Symbolicate} from './symbolicate';
const {
createActionStartEntry,
createActionEndEntry,
log,
} = require('../Logger');
function debounceAndBatch(fn, delay) {
let args = [];
let timeout;
return value => {
args.push(value);
clearTimeout(timeout);
timeout = setTimeout(() => {
const a = args;
args = [];
fn(a);
}, delay);
};
}
type Options = {
assetExts?: Array<string>,
blacklistRE?: RegExp,
cacheVersion?: string,
extraNodeModules?: {},
getTransformOptions?: GetTransformOptions,
globalTransformCache: ?GlobalTransformCache,
hasteImpl?: HasteImpl,
moduleFormat?: string,
platforms?: Array<string>,
polyfillModuleNames?: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache?: boolean,
silent?: boolean,
transformModulePath?: string,
transformTimeoutInterval?: number,
watch?: boolean,
};
const bundleOpts = declareOpts({
sourceMapUrl: {
type: 'string',
required: false,
},
entryFile: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
minify: {
type: 'boolean',
default: false,
},
runModule: {
type: 'boolean',
default: true,
},
inlineSourceMap: {
type: 'boolean',
default: false,
},
platform: {
type: 'string',
required: true,
},
runBeforeMainModule: {
type: 'array',
default: defaults.runBeforeMainModule,
},
unbundle: {
type: 'boolean',
default: false,
},
hot: {
type: 'boolean',
default: false,
},
entryModuleOnly: {
type: 'boolean',
default: false,
},
isolateModuleIDs: {
type: 'boolean',
default: false,
},
resolutionResponse: {
type: 'object',
},
generateSourceMaps: {
type: 'boolean',
required: false,
},
assetPlugins: {
type: 'array',
default: [],
},
onProgress: {
type: 'function',
},
});
const dependencyOpts = declareOpts({
platform: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
entryFile: {
type: 'string',
required: true,
},
recursive: {
type: 'boolean',
default: true,
},
hot: {
type: 'boolean',
default: false,
},
minify: {
type: 'boolean',
default: undefined,
},
});
const bundleDeps = new WeakMap();
const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
class Server {
_opts: {
assetExts: Array<string>,
blacklistRE: void | RegExp,
cacheVersion: string,
extraNodeModules: {},
getTransformOptions?: GetTransformOptions,
hasteImpl?: HasteImpl,
moduleFormat: string,
platforms: Array<string>,
polyfillModuleNames: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache: boolean,
silent: boolean,
transformModulePath: void | string,
transformTimeoutInterval: ?number,
watch: boolean,
};
_projectRoots: Array<string>;
_bundles: {};
_changeWatchers: Array<{
req: IncomingMessage,
res: ServerResponse,
}>;
_fileChangeListeners: Array<(filePath: string) => mixed>;
_assetServer: AssetServer;
_bundler: Bundler;
_debouncedFileChangeHandler: (filePath: string) => mixed;
_hmrFileChangeListener: (type: string, filePath: string) => mixed;
_reporter: Reporter;
_symbolicateInWorker: Symbolicate;
constructor(options: Options) {
this._opts = {
assetExts: options.assetExts || defaults.assetExts,
blacklistRE: options.blacklistRE,
cacheVersion: options.cacheVersion || '1.0',
extraNodeModules: options.extraNodeModules || {},
getTransformOptions: options.getTransformOptions,
globalTransformCache: options.globalTransformCache,
hasteImpl: options.hasteImpl,
moduleFormat: options.moduleFormat != null ? options.moduleFormat : 'haste',
platforms: options.platforms || defaults.platforms,
polyfillModuleNames: options.polyfillModuleNames || [],
projectRoots: options.projectRoots,
providesModuleNodeModules: options.providesModuleNodeModules,
reporter: options.reporter,
resetCache: options.resetCache || false,
silent: options.silent || false,
transformModulePath: options.transformModulePath,
transformTimeoutInterval: options.transformTimeoutInterval,
watch: options.watch || false,
};
const processFileChange =
({type, filePath, stat}) => this.onFileChange(type, filePath, stat);
this._reporter = options.reporter;
this._projectRoots = this._opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
this._fileChangeListeners = [];
this._assetServer = new AssetServer({
assetExts: this._opts.assetExts,
projectRoots: this._opts.projectRoots,
});
const bundlerOpts = Object.create(this._opts);
bundlerOpts.assetServer = this._assetServer;
bundlerOpts.allowBundleUpdates = this._opts.watch;
bundlerOpts.globalTransformCache = options.globalTransformCache;
bundlerOpts.watch = this._opts.watch;
bundlerOpts.reporter = options.reporter;
this._bundler = new Bundler(bundlerOpts);
// changes to the haste map can affect resolution of files in the bundle
const dependencyGraph = this._bundler.getResolver().getDependencyGraph();
dependencyGraph.load().then(() => {
dependencyGraph.getWatcher().on(
'change',
({eventsQueue}) => eventsQueue.forEach(processFileChange),
);
dependencyGraph.getHasteMap().on('change', () => {
debug('Clearing bundle cache due to haste map change');
this._clearBundles();
});
});
this._debouncedFileChangeHandler = debounceAndBatch(filePaths => {
// only clear bundles for non-JS changes
if (filePaths.every(RegExp.prototype.test, /\.js(?:on)?$/i)) {
for (const key in this._bundles) {
this._bundles[key].then(bundle => {
const deps = bundleDeps.get(bundle);
filePaths.forEach(filePath => {
// $FlowFixMe(>=0.37.0)
if (deps.files.has(filePath)) {
// $FlowFixMe(>=0.37.0)
deps.outdated.add(filePath);
}
});
}).catch(e => {
debug(`Could not update bundle: ${e}, evicting from cache`);
delete this._bundles[key];
});
}
} else {
debug('Clearing bundles due to non-JS change');
this._clearBundles();
}
this._informChangeWatchers();
}, 50);
this._symbolicateInWorker = symbolicate.createWorker();
}
end(): mixed {
return this._bundler.end();
}
setHMRFileChangeListener(
listener: (type: string, filePath: string) => mixed,
) {
this._hmrFileChangeListener = listener;
}
addFileChangeListener(listener: (filePath: string) => mixed) {
if (this._fileChangeListeners.indexOf(listener) === -1) {
this._fileChangeListeners.push(listener);
}
}
buildBundle(options: {
entryFile: string,
platform?: string,
}): Promise<Bundle> {
return this._bundler.getResolver().getDependencyGraph().load().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = bundleOpts(options);
return this._bundler.bundle(opts);
}).then(bundle => {
const modules = bundle.getModules();
const nonVirtual = modules.filter(m => !m.virtual);
bundleDeps.set(bundle, {
files: new Map(
nonVirtual
.map(({sourcePath, meta = {dependencies: []}}) =>
[sourcePath, meta.dependencies])
),
idToIndex: new Map(modules.map(({id}, i) => [id, i])),
dependencyPairs: new Map(
nonVirtual
.filter(({meta}) => meta && meta.dependencyPairs)
.map(m => [m.sourcePath, m.meta.dependencyPairs])
),
outdated: new Set(),
});
return bundle;
});
}
buildBundleFromUrl(reqUrl: string): Promise<mixed> {
const options = this._getOptionsFromUrl(reqUrl);
return this.buildBundle(options);
}
buildBundleForHMR(
options: {platform: ?string},
host: string,
port: number,
): Promise<string> {
return this._bundler.hmrBundle(options, host, port);
}
getShallowDependencies(options: {
entryFile: string,
platform?: string,
}): Promise<mixed> {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = dependencyOpts(options);
return this._bundler.getShallowDependencies(opts);
});
}
getModuleForPath(entryFile: string): Module {
return this._bundler.getModuleForPath(entryFile);
}
getDependencies(options: {
entryFile: string,
platform?: string,
}): Promise<ResolutionResponse> {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = dependencyOpts(options);
return this._bundler.getDependencies(opts);
});
}
getOrderedDependencyPaths(options: {}): Promise<mixed> {
return Promise.resolve().then(() => {
const opts = dependencyOpts(options);
return this._bundler.getOrderedDependencyPaths(opts);
});
}
onFileChange(type: string, filePath: string, stat: Stats) {
this._assetServer.onFileChange(type, filePath, stat);
this._bundler.invalidateFile(filePath);
// If Hot Loading is enabled avoid rebuilding bundles and sending live
// updates. Instead, send the HMR updates right away and clear the bundles
// cache so that if the user reloads we send them a fresh bundle
if (this._hmrFileChangeListener) {
// Clear cached bundles in case user reloads
this._clearBundles();
this._hmrFileChangeListener(type, filePath);
return;
} else if (type !== 'change' && filePath.indexOf(NODE_MODULES) !== -1) {
// node module resolution can be affected by added or removed files
debug('Clearing bundles due to potential node_modules resolution change');
this._clearBundles();
}
Promise.all(
this._fileChangeListeners.map(listener => listener(filePath))
).then(
() => this._onFileChangeComplete(filePath),
() => this._onFileChangeComplete(filePath)
);
}
_onFileChangeComplete(filePath: string) {
// Make sure the file watcher event runs through the system before
// we rebuild the bundles.
this._debouncedFileChangeHandler(filePath);
}
_clearBundles() {
this._bundles = Object.create(null);
}
_informChangeWatchers() {
const watchers = this._changeWatchers;
const headers = {
'Content-Type': 'application/json; charset=UTF-8',
};
watchers.forEach(function(w) {
w.res.writeHead(205, headers);
w.res.end(JSON.stringify({changed: true}));
});
this._changeWatchers = [];
}
_processDebugRequest(reqUrl: string, res: ServerResponse) {
let ret = '<!doctype html>';
const pathname = url.parse(reqUrl).pathname;
/* $FlowFixMe: pathname would be null for an invalid URL */
const parts = pathname.split('/').filter(Boolean);
if (parts.length === 1) {
ret += '<div><a href="/debug/bundles">Cached Bundles</a></div>';
res.end(ret);
} else if (parts[1] === 'bundles') {
ret += '<h1> Cached Bundles </h1>';
Promise.all(Object.keys(this._bundles).map(optionsJson =>
this._bundles[optionsJson].then(p => {
ret += '<div><h2>' + optionsJson + '</h2>';
ret += p.getDebugInfo();
})
)).then(
() => res.end(ret),
e => {
res.writeHead(500);
res.end('Internal Error');
terminal.log(e.stack); // eslint-disable-line no-console-disallow
}
);
} else {
res.writeHead(404);
res.end('Invalid debug request');
return;
}
}
_processOnChangeRequest(req: IncomingMessage, res: ServerResponse) {
const watchers = this._changeWatchers;
watchers.push({
req,
res,
});
req.on('close', () => {
for (let i = 0; i < watchers.length; i++) {
if (watchers[i] && watchers[i].req === req) {
watchers.splice(i, 1);
break;
}
}
});
}
_rangeRequestMiddleware(
req: IncomingMessage,
res: ServerResponse,
data: string,
assetPath: string,
) {
if (req.headers && req.headers.range) {
const [rangeStart, rangeEnd] = req.headers.range.replace(/bytes=/, '').split('-');
const dataStart = parseInt(rangeStart, 10);
const dataEnd = rangeEnd ? parseInt(rangeEnd, 10) : data.length - 1;
const chunksize = (dataEnd - dataStart) + 1;
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': chunksize.toString(),
'Content-Range': `bytes ${dataStart}-${dataEnd}/${data.length}`,
'Content-Type': mime.lookup(path.basename(assetPath[1])),
});
return data.slice(dataStart, dataEnd + 1);
}
return data;
}
_processAssetsRequest(req: IncomingMessage, res: ServerResponse) {
const urlObj = url.parse(decodeURI(req.url), true);
/* $FlowFixMe: could be empty if the url is invalid */
const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/);
const processingAssetRequestLogEntry =
log(createActionStartEntry({
action_name: 'Processing asset request',
asset: assetPath[1],
}));
/* $FlowFixMe: query may be empty for invalid URLs */
this._assetServer.get(assetPath[1], urlObj.query.platform)
.then(
data => {
// Tell clients to cache this for 1 year.
// This is safe as the asset url contains a hash of the asset.
if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) {
res.setHeader('Cache-Control', 'max-age=31536000');
}
res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => {
log(createActionEndEntry(processingAssetRequestLogEntry));
});
},
error => {
console.error(error.stack);
res.writeHead(404);
res.end('Asset not found');
}
);
}
optionsHash(options: {}) {
// onProgress is a function, can't be serialized
return JSON.stringify(Object.assign({}, options, {onProgress: null}));
}
/**
* Ensure we properly report the promise of a build that's happening,
* including failed builds. We use that separately for when we update a bundle
* and for when we build for scratch.
*/
_reportBundlePromise(
options: {entryFile: string},
bundlePromise: Promise<Bundle>,
): Promise<Bundle> {
this._reporter.update({
entryFilePath: options.entryFile,
type: 'bundle_build_started',
});
return bundlePromise.then(bundle => {
this._reporter.update({
entryFilePath: options.entryFile,
type: 'bundle_build_done',
});
return bundle;
}, error => {
this._reporter.update({
entryFilePath: options.entryFile,
error,
type: 'bundle_build_failed',
});
return Promise.reject(error);
});
}
_useCachedOrUpdateOrCreateBundle(options: {
entryFile: string,
platform?: string,
}): Promise<Bundle> {
const optionsJson = this.optionsHash(options);
const bundleFromScratch = () => {
const building = this.buildBundle(options);
this._bundles[optionsJson] = building;
return building;
};
if (optionsJson in this._bundles) {
return this._bundles[optionsJson].then(bundle => {
const deps = bundleDeps.get(bundle);
// $FlowFixMe(>=0.37.0)
const {dependencyPairs, files, idToIndex, outdated} = deps;
if (outdated.size) {
const updatingExistingBundleLogEntry =
log(createActionStartEntry({
action_name: 'Updating existing bundle',
outdated_modules: outdated.size,
}));
debug('Attempt to update existing bundle');
const changedModules =
Array.from(outdated, this.getModuleForPath, this);
// $FlowFixMe(>=0.37.0)
deps.outdated = new Set();
const opts = bundleOpts(options);
const {platform, dev, minify, hot} = opts;
// Need to create a resolution response to pass to the bundler
// to process requires after transform. By providing a
// specific response we can compute a non recursive one which
// is the least we need and improve performance.
const bundlePromise = this._bundles[optionsJson] =
this.getDependencies({
platform, dev, hot, minify,
entryFile: options.entryFile,
recursive: false,
}).then(response => {
debug('Update bundle: rebuild shallow bundle');
changedModules.forEach(m => {
response.setResolvedDependencyPairs(
m,
dependencyPairs.get(m.path),
{ignoreFinalized: true},
);
});
return this.buildBundle({
...options,
resolutionResponse: response.copy({
dependencies: changedModules,
}),
}).then(updateBundle => {
const oldModules = bundle.getModules();
const newModules = updateBundle.getModules();
for (let i = 0, n = newModules.length; i < n; i++) {
const moduleTransport = newModules[i];
const {meta, sourcePath} = moduleTransport;
if (outdated.has(sourcePath)) {
/* $FlowFixMe: `meta` could be empty */
if (!contentsEqual(meta.dependencies, new Set(files.get(sourcePath)))) {
// bail out if any dependencies changed
return Promise.reject(Error(
`Dependencies of ${sourcePath} changed from [${
/* $FlowFixMe: `get` can return empty */
files.get(sourcePath).join(', ')
}] to [${
/* $FlowFixMe: `meta` could be empty */
meta.dependencies.join(', ')
}]`
));
}
oldModules[idToIndex.get(moduleTransport.id)] = moduleTransport;
}
}
bundle.invalidateSource();
log(createActionEndEntry(updatingExistingBundleLogEntry));
debug('Successfully updated existing bundle');
return bundle;
});
}).catch(e => {
debug('Failed to update existing bundle, rebuilding...', e.stack || e.message);
return bundleFromScratch();
});
return this._reportBundlePromise(options, bundlePromise);
} else {
debug('Using cached bundle');
return bundle;
}
});
}
return this._reportBundlePromise(options, bundleFromScratch());
}
processRequest(
req: IncomingMessage,
res: ServerResponse,
next: () => mixed,
) {
const urlObj = url.parse(req.url, true);
const {host} = req.headers;
debug(`Handling request: ${host ? 'http://' + host : ''}${req.url}`);
/* $FlowFixMe: Could be empty if the URL is invalid. */
const pathname: string = urlObj.pathname;
let requestType;
if (pathname.match(/\.bundle$/)) {
requestType = 'bundle';
} else if (pathname.match(/\.map$/)) {
requestType = 'map';
} else if (pathname.match(/\.assets$/)) {
requestType = 'assets';
} else if (pathname.match(/^\/debug/)) {
this._processDebugRequest(req.url, res);
return;
} else if (pathname.match(/^\/onchange\/?$/)) {
this._processOnChangeRequest(req, res);
return;
} else if (pathname.match(/^\/assets\//)) {
this._processAssetsRequest(req, res);
return;
} else if (pathname === '/symbolicate') {
this._symbolicate(req, res);
return;
} else {
next();
return;
}
const options = this._getOptionsFromUrl(req.url);
const requestingBundleLogEntry =
log(createActionStartEntry({
action_name: 'Requesting bundle',
bundle_url: req.url,
entry_point: options.entryFile,
}));
let reportProgress = () => {};
if (!this._opts.silent) {
reportProgress = (transformedFileCount, totalFileCount) => {
this._reporter.update({
type: 'bundle_transform_progressed',
entryFilePath: options.entryFile,
transformedFileCount,
totalFileCount,
});
};
}
const mres = MultipartResponse.wrap(req, res);
options.onProgress = (done, total) => {
reportProgress(done, total);
mres.writeChunk({'Content-Type': 'application/json'}, JSON.stringify({done, total}));
};
debug('Getting bundle for request');
const building = this._useCachedOrUpdateOrCreateBundle(options);
building.then(
p => {
if (requestType === 'bundle') {
debug('Generating source code');
const bundleSource = p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
debug('Writing response headers');
const etag = p.getEtag();
mres.setHeader('Content-Type', 'application/javascript');
mres.setHeader('ETag', etag);
if (req.headers['if-none-match'] === etag) {
debug('Responding with 304');
mres.writeHead(304);
mres.end();
} else {
mres.end(bundleSource);
}
debug('Finished response');
log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'map') {
const sourceMap = p.getSourceMapString({
minify: options.minify,
dev: options.dev,
});
mres.setHeader('Content-Type', 'application/json');
mres.end(sourceMap);
log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'assets') {
const assetsList = JSON.stringify(p.getAssets());
mres.setHeader('Content-Type', 'application/json');
mres.end(assetsList);
log(createActionEndEntry(requestingBundleLogEntry));
}
},
error => this._handleError(mres, this.optionsHash(options), error)
).catch(error => {
process.nextTick(() => {
throw error;
});
});
}
_symbolicate(req: IncomingMessage, res: ServerResponse) {
const symbolicatingLogEntry =
log(createActionStartEntry('Symbolicating'));
debug('Start symbolication');
/* $FlowFixMe: where is `rowBody` defined? Is it added by
* the `connect` framework? */
Promise.resolve(req.rawBody).then(body => {
const stack = JSON.parse(body).stack;
// In case of multiple bundles / HMR, some stack frames can have
// different URLs from others
const urls = new Set();
stack.forEach(frame => {
const sourceUrl = frame.file;
// Skip `/debuggerWorker.js` which drives remote debugging because it
// does not need to symbolication.
// Skip anything except http(s), because there is no support for that yet
if (!urls.has(sourceUrl) &&
!sourceUrl.endsWith('/debuggerWorker.js') &&
sourceUrl.startsWith('http')) {
urls.add(sourceUrl);
}
});
const mapPromises =
Array.from(urls.values()).map(this._sourceMapForURL, this);
debug('Getting source maps for symbolication');
return Promise.all(mapPromises).then(maps => {
debug('Sending stacks and maps to symbolication worker');
const urlsToMaps = zip(urls.values(), maps);
return this._symbolicateInWorker(stack, urlsToMaps);
});
}).then(
stack => {
debug('Symbolication done');
res.end(JSON.stringify({stack}));
process.nextTick(() => {
log(createActionEndEntry(symbolicatingLogEntry));
});
},
error => {
console.error(error.stack || error);
res.statusCode = 500;
res.end(JSON.stringify({error: error.message}));
}
);
}
_sourceMapForURL(reqUrl: string): Promise<SourceMap> {
const options = this._getOptionsFromUrl(reqUrl);
const building = this._useCachedOrUpdateOrCreateBundle(options);
return building.then(p => p.getSourceMap({
minify: options.minify,
dev: options.dev,
}));
}
_handleError(res: ServerResponse, bundleID: string, error: {
status: number,
type: string,
description: string,
filename: string,
lineNumber: number,
errors: Array<{description: string, filename: string, lineNumber: number}>,
}) {
res.writeHead(error.status || 500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError' ||
error.type === 'NotFoundError' ||
error.type === 'UnableToResolveError') {
error.errors = [{
description: error.description,
filename: error.filename,
lineNumber: error.lineNumber,
}];
res.end(JSON.stringify(error));
if (error.type === 'NotFoundError') {
delete this._bundles[bundleID];
}
} else {
console.error(error.stack || error);
res.end(JSON.stringify({
type: 'InternalError',
message: 'react-packager has encountered an internal error, ' +
'please check your terminal error output for more details',
}));
}
}
_getOptionsFromUrl(reqUrl: string): {
sourceMapUrl: string,
entryFile: string,
dev: boolean,
minify: boolean,
hot: boolean,
runModule: boolean,
inlineSourceMap: boolean,
platform?: string,
entryModuleOnly: boolean,
generateSourceMaps: boolean,
assetPlugins: Array<string>,
onProgress?: (doneCont: number, totalCount: number) => mixed,
} {
// `true` to parse the query param as an object.
const urlObj = url.parse(reqUrl, true);
/* $FlowFixMe: `pathname` could be empty for an invalid URL */
const pathname = decodeURIComponent(urlObj.pathname);
// Backwards compatibility. Options used to be as added as '.' to the
// entry module name. We can safely remove these options.
const entryFile = pathname.replace(/^\//, '').split('.').filter(part => {
if (part === 'includeRequire' || part === 'runModule' ||
part === 'bundle' || part === 'map' || part === 'assets') {
return false;
}
return true;
}).join('.') + '.js';
// try to get the platform from the url
/* $FlowFixMe: `query` could be empty for an invalid URL */
const platform = urlObj.query.platform ||
getPlatformExtension(pathname);
/* $FlowFixMe: `query` could be empty for an invalid URL */
const assetPlugin = urlObj.query.assetPlugin;
const assetPlugins = Array.isArray(assetPlugin) ?
assetPlugin :
(typeof assetPlugin === 'string') ? [assetPlugin] : [];
const dev = this._getBoolOptionFromQuery(urlObj.query, 'dev', true);
const minify = this._getBoolOptionFromQuery(urlObj.query, 'minify', false);
return {
sourceMapUrl: url.format({
hash: urlObj.hash,
pathname: pathname.replace(/\.bundle$/, '.map'),
query: urlObj.query,
search: urlObj.search,
}),
entryFile,
dev,
minify,
hot: this._getBoolOptionFromQuery(urlObj.query, 'hot', false),
runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: this._getBoolOptionFromQuery(
urlObj.query,
'inlineSourceMap',
false
),
platform,
entryModuleOnly: this._getBoolOptionFromQuery(
urlObj.query,
'entryModuleOnly',
false,
),
generateSourceMaps:
minify || !dev || this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap', false),
assetPlugins,
};
}
_getBoolOptionFromQuery(query: ?{}, opt: string, defaultVal: boolean): boolean {
/* $FlowFixMe: `query` could be empty when it comes from an invalid URL */
if (query[opt] == null) {
return defaultVal;
}
return query[opt] === 'true' || query[opt] === '1';
}
}
function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean {
return array.length === set.size && array.every(set.has, set);
}
function* zip<X, Y>(xs: Iterable<X>, ys: Iterable<Y>): Iterable<[X, Y]> {
//$FlowIssue #9324959
const ysIter: Iterator<Y> = ys[Symbol.iterator]();
for (const x of xs) {
const y = ysIter.next();
if (y.done) {
return;
}
yield [x, y.value];
}
}
module.exports = Server;