UNPKG

xdl

Version:
487 lines (476 loc) 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.broadcastMessage = broadcastMessage; exports.bundleAsync = bundleAsync; exports.getUrlAsync = getUrlAsync; exports.openAsync = openAsync; exports.startAsync = startAsync; exports.stopAsync = stopAsync; function devcert() { const data = _interopRequireWildcard(require("@expo/devcert")); devcert = function () { return data; }; return data; } function _betterOpn() { const data = _interopRequireDefault(require("better-opn")); _betterOpn = function () { return data; }; return data; } function _chalk() { const data = _interopRequireDefault(require("chalk")); _chalk = function () { return data; }; return data; } function _fsExtra() { const data = _interopRequireDefault(require("fs-extra")); _fsExtra = function () { return data; }; return data; } function _getenv() { const data = _interopRequireDefault(require("getenv")); _getenv = function () { return data; }; return data; } function path() { const data = _interopRequireWildcard(require("path")); path = function () { return data; }; return data; } function _webpack() { const data = _interopRequireDefault(require("webpack")); _webpack = function () { return data; }; return data; } function _webpackDevServer() { const data = _interopRequireDefault(require("webpack-dev-server")); _webpackDevServer = function () { return data; }; return data; } function _internal() { const data = require("./internal"); _internal = function () { return data; }; return data; } function _formatWebpackMessages() { const data = require("./webpack-utils/formatWebpackMessages"); _formatWebpackMessages = function () { return data; }; return data; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const WEBPACK_LOG_TAG = 'expo'; let webpackDevServerInstance = null; let webpackServerPort = null; // A custom message websocket broadcaster used to send messages to a React Native runtime. let customMessageSocketBroadcaster; async function clearWebCacheAsync(projectRoot, mode) { const cacheFolder = path().join(projectRoot, '.expo', 'web', 'cache', mode); _internal().ProjectUtils.logInfo(projectRoot, WEBPACK_LOG_TAG, _chalk().default.dim(`Clearing ${mode} cache directory...`)); try { await _fsExtra().default.remove(cacheFolder); } catch {} } async function broadcastMessage(message, data) { if (!webpackDevServerInstance || !(webpackDevServerInstance instanceof _webpackDevServer().default)) { return; } // Allow any message on native if (customMessageSocketBroadcaster) { customMessageSocketBroadcaster(message, data); // return; } if (message !== 'reload') { // TODO: // Webpack currently only supports reloading the client (browser), // remove this when we have custom sockets, and native support. return; } // TODO: // Default webpack-dev-server sockets use "content-changed" instead of "reload" (what we use on native). // For now, just manually convert the value so our CLI interface can be unified. const hackyConvertedMessage = message === 'reload' ? 'content-changed' : message; // @ts-ignore: type not exposed const connections = webpackDevServerInstance.webSocketServer.clients; if (!connections.length) { console.warn('No HMR clients are connected to the dev server'); } webpackDevServerInstance.sendMessage(connections, hackyConvertedMessage, data); } async function startAsync(projectRoot, options = {}) { await stopAsync(projectRoot); if (webpackDevServerInstance) { _internal().ProjectUtils.logError(projectRoot, WEBPACK_LOG_TAG, _chalk().default.red(`Webpack is already running.`)); return null; } const fullOptions = transformCLIOptions(options); const env = await getWebpackConfigEnvFromBundlingOptionsAsync(projectRoot, fullOptions); if (fullOptions.clear) { await clearWebCacheAsync(projectRoot, env.mode); } if (env.https) { if (!process.env.SSL_CRT_FILE || !process.env.SSL_KEY_FILE) { const ssl = await getSSLCertAsync({ name: 'localhost', directory: projectRoot }); if (ssl) { process.env.SSL_CRT_FILE = ssl.certPath; process.env.SSL_KEY_FILE = ssl.keyPath; } } } const port = await getAvailablePortAsync({ projectRoot, defaultPort: options.port }); webpackServerPort = port; _internal().ProjectUtils.logInfo(projectRoot, WEBPACK_LOG_TAG, `Starting Webpack on port ${webpackServerPort} in ${_chalk().default.underline(env.mode)} mode.`); const protocol = env.https ? 'https' : 'http'; const config = await loadConfigAsync({ ...env, platform: 'web' }); // Create a webpack compiler const compiler = (0, _webpack().default)(config); const server = new (_webpackDevServer().default)({ ...config.devServer, port, host: _internal().WebpackEnvironment.WEB_HOST }, compiler); try { // Launch WebpackDevServer. await server.start(); if (typeof options.onWebpackFinished === 'function') { options.onWebpackFinished(); } } catch (error) { _internal().ProjectUtils.logError(projectRoot, WEBPACK_LOG_TAG, error.message); if (typeof options.onWebpackFinished === 'function') { options.onWebpackFinished(error); } } webpackDevServerInstance = server; await _internal().ProjectSettings.setPackagerInfoAsync(projectRoot, { webpackServerPort }); const host = _internal().ip.address(); const url = `${protocol}://${host}:${port}`; // Extend the close method to ensure that we clean up the local info. const originalClose = server.stopCallback.bind(server); server.stopCallback = callback => { return originalClose(err => { _internal().ProjectSettings.setPackagerInfoAsync(projectRoot, { webpackServerPort: null }).finally(() => { callback === null || callback === void 0 ? void 0 : callback(err); webpackDevServerInstance = null; webpackServerPort = null; }); }); }; return { server, location: { url, port, protocol, host }, // Match the native protocol. messageSocket: { broadcast: broadcastMessage } }; } async function stopAsync(projectRoot) { if (webpackDevServerInstance) { await new Promise(res => { if (webpackDevServerInstance) { _internal().ProjectUtils.logInfo(projectRoot, WEBPACK_LOG_TAG, '\u203A Stopping Webpack server'); webpackDevServerInstance.stopCallback(res); } }); } } // TODO: Reuse logging system that we use in dev server + extras async function compileWebAppAsync(projectRoot, compiler) { // We generate the stats.json file in the webpack-config const { warnings } = await new Promise((resolve, reject) => compiler.run((error, stats) => { var _messages, _messages$errors, _messages2, _messages2$warnings; let messages; if (error) { if (!error.message) { return reject(error); } messages = (0, _formatWebpackMessages().formatWebpackMessages)({ // @ts-ignore: TODO errors: [error.message], warnings: [], _showErrors: true, _showWarnings: true }); } else { messages = (0, _formatWebpackMessages().formatWebpackMessages)(stats === null || stats === void 0 ? void 0 : stats.toJson({ all: false, warnings: true, errors: true })); } if ((_messages = messages) !== null && _messages !== void 0 && (_messages$errors = _messages.errors) !== null && _messages$errors !== void 0 && _messages$errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. if (messages.errors.length > 1) { messages.errors.length = 1; } return reject(new (_internal().XDLError)('WEBPACK_BUNDLE', messages.errors.join('\n\n'))); } if (_getenv().default.boolish('EXPO_WEB_BUILD_STRICT', false) && _getenv().default.boolish('CI', false) && (_messages2 = messages) !== null && _messages2 !== void 0 && (_messages2$warnings = _messages2.warnings) !== null && _messages2$warnings !== void 0 && _messages2$warnings.length) { _internal().ProjectUtils.logWarning(projectRoot, WEBPACK_LOG_TAG, _chalk().default.yellow('\nTreating warnings as errors because `process.env.CI = true` and `process.env.EXPO_WEB_BUILD_STRICT = true`. \n' + 'Most CI servers set it automatically.\n')); return reject(new (_internal().XDLError)('WEBPACK_BUNDLE', messages.warnings.join('\n\n'))); } resolve({ warnings: messages.warnings }); })); return { warnings }; } async function bundleWebAppAsync(projectRoot, config) { const compiler = (0, _webpack().default)(config); try { const { warnings } = await compileWebAppAsync(projectRoot, compiler); if (warnings.length) { _internal().ProjectUtils.logWarning(projectRoot, WEBPACK_LOG_TAG, _chalk().default.yellow('Compiled with warnings.\n')); _internal().ProjectUtils.logWarning(projectRoot, WEBPACK_LOG_TAG, warnings.join('\n\n')); } else { _internal().ProjectUtils.logInfo(projectRoot, WEBPACK_LOG_TAG, _chalk().default.green('Compiled successfully.\n')); } } catch (error) { _internal().ProjectUtils.logError(projectRoot, WEBPACK_LOG_TAG, _chalk().default.red('Failed to compile.\n')); throw error; } } async function bundleAsync(projectRoot, options) { const fullOptions = transformCLIOptions({ ...options }); const env = await getWebpackConfigEnvFromBundlingOptionsAsync(projectRoot, { ...fullOptions, // Force production mode: 'production' }); // @ts-ignore if (typeof env.offline !== 'undefined') { throw new Error('offline support must be added manually: https://expo.fyi/enabling-web-service-workers'); } if (fullOptions.clear) { await clearWebCacheAsync(projectRoot, env.mode); } const config = await loadConfigAsync(env); await bundleWebAppAsync(projectRoot, config); } /** * Get the URL for the running instance of Webpack dev server. * * @param projectRoot */ async function getUrlAsync(projectRoot) { if (!webpackDevServerInstance) { return null; } const host = _internal().ip.address(); const protocol = await getProtocolAsync(projectRoot); return `${protocol}://${host}:${webpackServerPort}`; } async function getProtocolAsync(projectRoot) { // TODO: Bacon: Handle when not in expo const { https } = await _internal().ProjectSettings.readAsync(projectRoot); return https === true ? 'https' : 'http'; } async function getAvailablePortAsync(options) { try { const defaultPort = 'defaultPort' in options && options.defaultPort ? options.defaultPort : _internal().WebpackEnvironment.DEFAULT_PORT; const port = await (0, _internal().choosePortAsync)(options.projectRoot, { defaultPort, host: 'host' in options && options.host ? options.host : _internal().WebpackEnvironment.WEB_HOST }); if (!port) { throw new Error(`Port ${defaultPort} not available.`); } return port; } catch (error) { throw new (_internal().XDLError)('NO_PORT_FOUND', error.message); } } function setMode(mode) { process.env.BABEL_ENV = mode; process.env.NODE_ENV = mode; } function validateBoolOption(name, value, defaultValue) { if (typeof value === 'undefined') { value = defaultValue; } if (typeof value !== 'boolean') { throw new (_internal().XDLError)('WEBPACK_INVALID_OPTION', `'${name}' option must be a boolean.`); } return value; } function transformCLIOptions(options) { // Transform the CLI flags into more explicit values return { ...options, isImageEditingEnabled: options.pwa }; } async function applyOptionsToProjectSettingsAsync(projectRoot, options) { const newSettings = {}; // Change settings before reading them if (typeof options.https === 'boolean') { newSettings.https = options.https; } if (Object.keys(newSettings).length) { await _internal().ProjectSettings.setAsync(projectRoot, newSettings); } return await _internal().ProjectSettings.readAsync(projectRoot); } async function getWebpackConfigEnvFromBundlingOptionsAsync(projectRoot, options) { const { dev, https } = await applyOptionsToProjectSettingsAsync(projectRoot, options); const mode = typeof options.mode === 'string' ? options.mode : dev ? 'development' : 'production'; const isImageEditingEnabled = validateBoolOption('isImageEditingEnabled', options.isImageEditingEnabled, true); return { projectRoot, pwa: isImageEditingEnabled, isImageEditingEnabled, mode, https, logger: _internal().ProjectUtils.getLogger(projectRoot), port: options.port, ...(options.webpackEnv || {}) }; } async function getSSLCertAsync({ name, directory }) { console.log(_chalk().default.magenta`Ensuring auto SSL certificate is created (you might need to re-run with sudo)`); try { const result = await devcert().certificateFor(name); if (result) { const { key, cert } = result; const folder = path().join(directory, '.expo', 'web', 'development', 'ssl'); await _fsExtra().default.ensureDir(folder); const keyPath = path().join(folder, `key-${name}.pem`); await _fsExtra().default.writeFile(keyPath, key); const certPath = path().join(folder, `cert-${name}.pem`); await _fsExtra().default.writeFile(certPath, cert); return { keyPath, certPath }; } return result; } catch (error) { console.log(`Error creating SSL certificates: ${error}`); } return false; } function applyEnvironmentVariables(config) { // Use EXPO_DEBUG_WEB=true to enable debugging features for cases where the prod build // has errors that aren't caught in development mode. // Related: https://github.com/expo/expo-cli/issues/614 if (_internal().WebpackEnvironment.isDebugModeEnabled() && config.mode === 'production') { console.log(_chalk().default.bgYellow.black('Bundling the project in debug mode.')); const output = config.output || {}; const optimization = config.optimization || {}; // Add comments that describe the file import/exports. // This will make it easier to debug. output.pathinfo = true; // Readable ids for better debugging. optimization.moduleIds = 'named'; // if optimization.namedChunks is enabled optimization.chunkIds is set to 'named'. // This will manually enable it just to be safe. optimization.chunkIds = 'named'; Object.assign(config, { output, optimization }); } return config; } async function loadConfigAsync(env, argv) { setMode(env.mode); // Check if the project has a webpack.config.js in the root. const projectWebpackConfig = path().resolve(env.projectRoot, 'webpack.config.js'); let config; if (_fsExtra().default.existsSync(projectWebpackConfig)) { const webpackConfig = require(projectWebpackConfig); if (typeof webpackConfig === 'function') { config = await webpackConfig(env, argv); } else { config = webpackConfig; } } else { // Fallback to the default expo webpack config. const loadDefaultConfigAsync = require('@expo/webpack-config'); config = await loadDefaultConfigAsync(env, argv); } return applyEnvironmentVariables(config); } async function openAsync(projectRoot) { try { const url = await _internal().UrlUtils.constructWebAppUrlAsync(projectRoot, { hostType: 'localhost' }); if (!url) { throw new Error('Webpack Dev Server is not running'); } (0, _betterOpn().default)(url); return { success: true, url }; } catch (e) { _internal().Logger.global.error(`Couldn't start project on web: ${e.message}`); return { success: false, error: e }; } } //# sourceMappingURL=Webpack.js.map