UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

437 lines • 23.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebpackCompilerService = void 0; const path = require("path"); const semver = require("semver"); const _ = require("lodash"); const events_1 = require("events"); const decorators_1 = require("../../common/decorators"); const constants_1 = require("../../constants"); const yok_1 = require("../../common/yok"); const package_path_helper_1 = require("../../helpers/package-path-helper"); class WebpackCompilerService extends events_1.EventEmitter { constructor($options, $errors, $childProcess, $fs, $hooksService, $hostInfo, $logger, $mobileHelper, $cleanupService, $packageManager, $packageInstallationManager // private $sharedEventBus: ISharedEventBus ) { super(); this.$options = $options; this.$errors = $errors; this.$childProcess = $childProcess; this.$fs = $fs; this.$hooksService = $hooksService; this.$hostInfo = $hostInfo; this.$logger = $logger; this.$mobileHelper = $mobileHelper; this.$cleanupService = $cleanupService; this.$packageManager = $packageManager; this.$packageInstallationManager = $packageInstallationManager; this.webpackProcesses = {}; this.expectedHashes = {}; } async compileWithWatch(platformData, projectData, prepareData) { return new Promise(async (resolve, reject) => { if (this.webpackProcesses[platformData.platformNameLowerCase]) { resolve(void 0); return; } let isFirstWebpackWatchCompilation = true; prepareData.watch = true; try { const childProcess = await this.startWebpackProcess(platformData, projectData, prepareData); childProcess.stdout.on("data", function (data) { process.stdout.write(data); }); childProcess.stderr.on("data", function (data) { process.stderr.write(data); }); childProcess.on("message", (message) => { this.$logger.trace("Message from webpack", message); // if we are on webpack5 - we handle HMR in a slightly different way if (typeof message === "object" && "version" in message && "type" in message) { // first compilation can be ignored because it will be synced regardless // handling it here would trigger 2 syncs if (isFirstWebpackWatchCompilation) { isFirstWebpackWatchCompilation = false; resolve(childProcess); return; } // if ((message as IWebpackMessage).type === "hmr-status") { // // we pass message through our event-bus to be handled wherever needed // // in this case webpack-hmr-status-service listens for this event // this.$sharedEventBus.emit("webpack:hmr-status", message); // return; // } return this.handleHMRMessage(message, platformData, projectData, prepareData); } if (message === "Webpack compilation complete.") { this.$logger.info("Webpack build done!"); resolve(childProcess); } message = message; if (message.emittedFiles) { if (isFirstWebpackWatchCompilation) { isFirstWebpackWatchCompilation = false; this.expectedHashes[platformData.platformNameLowerCase] = prepareData.hmr ? message.hash : ""; return; } // Persist the previousHash value before calling `this.getUpdatedEmittedFiles` as it will modify the expectedHashes object with the current hash const previousHash = this.expectedHashes[platformData.platformNameLowerCase]; let result; if (prepareData.hmr) { result = this.getUpdatedEmittedFiles(message.emittedFiles, message.chunkFiles, message.hash, platformData.platformNameLowerCase); } else { result = { emittedFiles: message.emittedFiles, fallbackFiles: [], hash: "", }; } const files = result.emittedFiles.map((file) => path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, file)); const fallbackFiles = result.fallbackFiles.map((file) => path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, file)); const data = { files, hasOnlyHotUpdateFiles: files.every((f) => f.indexOf("hot-update") > -1), hmrData: { hash: result.hash, fallbackFiles, }, platform: platformData.platformNameLowerCase, }; this.$logger.trace("Generated data from webpack message:", data); // the hash of the compilation is the same as the previous one and there are only hot updates produced if (data.hasOnlyHotUpdateFiles && previousHash === message.hash) { return; } if (data.files.length) { this.emit(constants_1.WEBPACK_COMPILATION_COMPLETE, data); } } }); childProcess.on("error", (err) => { this.$logger.trace(`Unable to start webpack process in watch mode. Error is: ${err}`); delete this.webpackProcesses[platformData.platformNameLowerCase]; reject(err); }); childProcess.on("close", async (arg) => { await this.$cleanupService.removeKillProcess(childProcess.pid.toString()); const exitCode = typeof arg === "number" ? arg : arg && arg.code; this.$logger.trace(`Webpack process exited with code ${exitCode} when we expected it to be long living with watch.`); const error = new Error(`Executing webpack failed with exit code ${exitCode}.`); error.code = exitCode; delete this.webpackProcesses[platformData.platformNameLowerCase]; reject(error); }); } catch (err) { reject(err); } }); } async compileWithoutWatch(platformData, projectData, prepareData) { return new Promise(async (resolve, reject) => { if (this.webpackProcesses[platformData.platformNameLowerCase]) { resolve(); return; } try { const childProcess = await this.startWebpackProcess(platformData, projectData, prepareData); childProcess.on("error", (err) => { this.$logger.trace(`Unable to start webpack process in non-watch mode. Error is: ${err}`); delete this.webpackProcesses[platformData.platformNameLowerCase]; reject(err); }); childProcess.on("close", async (arg) => { await this.$cleanupService.removeKillProcess(childProcess.pid.toString()); delete this.webpackProcesses[platformData.platformNameLowerCase]; const exitCode = typeof arg === "number" ? arg : arg && arg.code; if (exitCode === 0) { resolve(); } else { const error = new Error(`Executing webpack failed with exit code ${exitCode}.`); error.code = exitCode; reject(error); } }); } catch (err) { reject(err); } }); } async stopWebpackCompiler(platform) { if (platform) { await this.stopWebpackForPlatform(platform); } else { const webpackedPlatforms = Object.keys(this.webpackProcesses); for (let i = 0; i < webpackedPlatforms.length; i++) { await this.stopWebpackForPlatform(webpackedPlatforms[i]); } } } async shouldUsePreserveSymlinksOption() { // pnpm does not require symlink (https://github.com/nodejs/node-eps/issues/46#issuecomment-277373566) // and it also does not work in some cases. // Check https://github.com/NativeScript/nativescript-cli/issues/5259 for more information const currentPackageManager = await this.$packageManager.getPackageManagerName(); const res = currentPackageManager !== constants_1.PackageManagers.pnpm; return res; } async startWebpackProcess(platformData, projectData, prepareData) { if (!this.$fs.exists(projectData.webpackConfigPath)) { this.$errors.fail(`The webpack configuration file ${projectData.webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${constants_1.CONFIG_FILE_NAME_DISPLAY}.`); } const envData = this.buildEnvData(platformData.platformNameLowerCase, projectData, prepareData); const envParams = await this.buildEnvCommandLineParams(envData, platformData, projectData, prepareData); const additionalNodeArgs = semver.major(process.version) <= 8 ? ["--harmony"] : []; if (await this.shouldUsePreserveSymlinksOption()) { additionalNodeArgs.push("--preserve-symlinks"); } if (process.arch === "x64") { additionalNodeArgs.unshift("--max_old_space_size=4096"); } const args = [ ...additionalNodeArgs, this.getWebpackExecutablePath(projectData), this.isWebpack5(projectData) ? `build` : null, `--config=${projectData.webpackConfigPath}`, ...envParams, ].filter(Boolean); if (prepareData.watch) { args.push("--watch"); } const stdio = prepareData.watch ? ["ipc"] : "inherit"; const options = { cwd: projectData.projectDir, stdio, }; options.env = { NATIVESCRIPT_WEBPACK_ENV: JSON.stringify(envData), }; if (this.$hostInfo.isWindows) { Object.assign(options.env, { APPDATA: process.env.appData }); } if (this.$options.hostProjectPath) { Object.assign(options.env, { USER_PROJECT_PLATFORMS_ANDROID: this.$options.hostProjectPath, USER_PROJECT_PLATFORMS_ANDROID_MODULE: this.$options.hostProjectModuleName, USER_PROJECT_PLATFORMS_IOS: this.$options.hostProjectPath, }); } const childProcess = this.$childProcess.spawn(process.execPath, args, options); this.webpackProcesses[platformData.platformNameLowerCase] = childProcess; await this.$cleanupService.addKillProcess(childProcess.pid.toString()); return childProcess; } buildEnvData(platform, projectData, prepareData) { var _a, _b, _c; const { env } = prepareData; const envData = Object.assign({}, env, { [platform.toLowerCase()]: true }); const appId = projectData.projectIdentifiers[platform]; const appPath = projectData.getAppDirectoryRelativePath(); const appResourcesPath = projectData.getAppResourcesRelativeDirectoryPath(); Object.assign(envData, appId && { appId }, appPath && { appPath }, appResourcesPath && { appResourcesPath }, { nativescriptLibPath: path.resolve(__dirname, "..", "..", "nativescript-cli-lib.js"), }); envData.verbose = envData.verbose || this.$logger.isVerbose(); envData.production = envData.production || prepareData.release; // add the config file name to the env data so the webpack process can read the // correct config file when resolving the CLI lib and the config service // we are explicitly setting it to false to force using the defaults envData.config = (_b = (_a = process.env.NATIVESCRIPT_CONFIG_NAME) !== null && _a !== void 0 ? _a : this.$options.config) !== null && _b !== void 0 ? _b : "false"; // explicitly set the env variable process.env.NATIVESCRIPT_CONFIG_NAME = envData.config; // The snapshot generation is wrongly located in the Webpack plugin. // It should be moved in the Native Prepare of the CLI or a Gradle task in the Runtime. // As a workaround, we skip the mksnapshot, xxd and android-ndk calls based on skipNativePrepare. // In this way the plugin will prepare only the snapshot JS entry without any native prepare and // we will able to execute cloud builds with snapshot without having any local snapshot or Docker setup. // TODO: Remove this flag when we remove the native part from the plugin. envData.skipSnapshotTools = prepareData.nativePrepare && prepareData.nativePrepare.skipNativePrepare; // only set sourceMap if not explicitly set through a flag if (typeof ((_c = prepareData === null || prepareData === void 0 ? void 0 : prepareData.env) === null || _c === void 0 ? void 0 : _c.sourceMap) === "undefined") { if (!prepareData.release) { envData.sourceMap = true; } } // convert string to boolean if (envData.sourceMap === "true" || envData.sourceMap === "false") { envData.sourceMap = envData.sourceMap === "true"; } if (prepareData.uniqueBundle > 0) { envData.uniqueBundle = prepareData.uniqueBundle; } return envData; } async buildEnvCommandLineParams(envData, platformData, projectData, prepareData) { const envFlagNames = Object.keys(envData); const canSnapshot = prepareData.release && this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName); if (envData && envData.snapshot) { if (!canSnapshot) { this.$logger.warn("Stripping the snapshot flag. " + "Bear in mind that snapshot is only available in Android release builds."); envFlagNames.splice(envFlagNames.indexOf("snapshot"), 1); } else if (this.$hostInfo.isWindows) { const minWebpackPluginWithWinSnapshotsVersion = "1.3.0"; const installedWebpackPluginVersion = await this.$packageInstallationManager.getInstalledDependencyVersion(constants_1.WEBPACK_PLUGIN_NAME, projectData.projectDir); const hasWebpackPluginWithWinSnapshotsSupport = !!installedWebpackPluginVersion ? semver.gte(semver.coerce(installedWebpackPluginVersion), minWebpackPluginWithWinSnapshotsVersion) : true; if (!hasWebpackPluginWithWinSnapshotsSupport) { this.$errors.fail(`In order to generate Snapshots on Windows, please upgrade your Webpack plugin version (npm i ${constants_1.WEBPACK_PLUGIN_NAME}@latest).`); } } } const args = []; envFlagNames.map((item) => { let envValue = envData[item]; if (typeof envValue === "undefined") { return; } if (typeof envValue === "boolean") { if (envValue) { args.push(`--env.${item}`); } } else { if (!Array.isArray(envValue)) { envValue = [envValue]; } envValue.map((value) => args.push(`--env.${item}=${value}`)); } }); return args; } getUpdatedEmittedFiles(allEmittedFiles, chunkFiles, nextHash, platform) { const currentHash = this.getCurrentHotUpdateHash(allEmittedFiles); // This logic is needed as there are already cases when webpack doesn't emit any files physically. // We've set noEmitOnErrors in webpack.config.js based on noEmitOnError from tsconfig.json, // so webpack doesn't emit any files when noEmitOnErrors: true is set in webpack.config.js and // there is a compilation error in the source code. On the other side, hmr generates new hot-update files // on every change and the hash of the next hmr update is written inside hot-update.json file. // Although webpack doesn't emit any files, hmr hash is still generated. The hash is generated per compilation no matter // if files will be emitted or not. This way, the first successful compilation after fixing the compilation error generates // a hash that is not the same as the one expected in the latest emitted hot-update.json file. // As a result, the hmr chain is broken and the changes are not applied. const isHashValid = nextHash ? this.expectedHashes[platform] === currentHash : true; this.expectedHashes[platform] = nextHash; const emittedHotUpdatesAndAssets = isHashValid ? _.difference(allEmittedFiles, chunkFiles) : allEmittedFiles; const fallbackFiles = chunkFiles.concat(emittedHotUpdatesAndAssets.filter((f) => f.indexOf("hot-update") === -1)); return { emittedFiles: emittedHotUpdatesAndAssets, fallbackFiles, hash: currentHash, }; } getCurrentHotUpdateHash(emittedFiles) { let hotHash; const hotUpdateScripts = emittedFiles.filter((x) => x.endsWith(".hot-update.js")); if (hotUpdateScripts && hotUpdateScripts.length) { // the hash is the same for each hot update in the current compilation const hotUpdateName = hotUpdateScripts[0]; const matcher = /^(.+)\.(.+)\.hot-update/gm; const matches = matcher.exec(hotUpdateName); hotHash = matches[2]; } return hotHash || ""; } async stopWebpackForPlatform(platform) { this.$logger.trace(`Stopping webpack watch for platform ${platform}.`); const webpackProcess = this.webpackProcesses[platform]; await this.$cleanupService.removeKillProcess(webpackProcess.pid.toString()); if (webpackProcess) { webpackProcess.kill("SIGINT"); delete this.webpackProcesses[platform]; } } handleHMRMessage(message, platformData, projectData, prepareData) { // handle new webpack hmr packets this.$logger.trace("Received message from webpack process:", message); if (message.type !== "compilation") { return; } this.$logger.trace("Webpack build done!"); const files = message.data.emittedAssets.map((asset) => path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, asset)); const staleFiles = message.data.staleAssets.map((asset) => path.join(platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, asset)); // extract last hash from emitted filenames const lastHash = (() => { const absoluteFileNameWithLastHash = files.find((fileName) => fileName.endsWith("hot-update.js")); if (!absoluteFileNameWithLastHash) { return null; } const fileNameWithLastHash = path.basename(absoluteFileNameWithLastHash); const matches = fileNameWithLastHash.match(/\.(.+).hot-update\.js/); if (matches) { return matches[1]; } })(); if (!files.length) { // ignore compilations if no new files are emitted return; } this.emit(constants_1.WEBPACK_COMPILATION_COMPLETE, { files, staleFiles, hasOnlyHotUpdateFiles: prepareData.hmr, hmrData: { hash: lastHash || message.hash, fallbackFiles: [], }, platform: platformData.platformNameLowerCase, }); } getWebpackExecutablePath(projectData) { if (this.isWebpack5(projectData)) { const packagePath = (0, package_path_helper_1.resolvePackagePath)("@nativescript/webpack", { paths: [projectData.projectDir], }); if (packagePath) { return path.resolve(packagePath, "dist", "bin", "index.js"); } } const packagePath = (0, package_path_helper_1.resolvePackagePath)("webpack", { paths: [projectData.projectDir], }); if (!packagePath) { return ""; } return path.resolve(packagePath, "bin", "webpack.js"); } isWebpack5(projectData) { const packageJSONPath = (0, package_path_helper_1.resolvePackageJSONPath)("@nativescript/webpack", { paths: [projectData.projectDir], }); if (packageJSONPath) { const packageData = this.$fs.readJson(packageJSONPath); const ver = semver.coerce(packageData.version); if (semver.satisfies(ver, ">= 5.0.0")) { return true; } } return false; } } exports.WebpackCompilerService = WebpackCompilerService; __decorate([ (0, decorators_1.performanceLog)() ], WebpackCompilerService.prototype, "startWebpackProcess", null); yok_1.injector.register("webpackCompilerService", WebpackCompilerService); //# sourceMappingURL=webpack-compiler-service.js.map