From 2abfb346e2477aa7c183ee622035fcf3a01b4d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20de=20Dios=20Mart=C3=ADnez=20Vallejo?= Date: Tue, 25 Mar 2025 00:40:35 +0100 Subject: [PATCH 1/3] feat: replace webpack-compiler-service by bundler-compiler-service --- lib/bootstrap.ts | 4 +- lib/constants.ts | 3 + lib/controllers/prepare-controller.ts | 16 +- lib/definitions/project.d.ts | 13 +- lib/project-data.ts | 9 + .../bundler-compiler-service.ts} | 188 +++-- .../webpack.d.ts => bundler/bundler.ts} | 42 +- .../bundler_Asdf/bundler-compiler-service.ts | 727 ++++++++++++++++++ lib/services/bundler_Asdf/bundler.d.ts | 226 ++++++ test/controllers/prepare-controller.ts | 6 +- .../bundler-compiler-service.ts} | 113 +-- test/stubs.ts | 2 + 12 files changed, 1177 insertions(+), 172 deletions(-) rename lib/services/{webpack/webpack-compiler-service.ts => bundler/bundler-compiler-service.ts} (77%) rename lib/services/{webpack/webpack.d.ts => bundler/bundler.ts} (90%) create mode 100644 lib/services/bundler_Asdf/bundler-compiler-service.ts create mode 100644 lib/services/bundler_Asdf/bundler.d.ts rename test/services/{webpack/webpack-compiler-service.ts => bundler/bundler-compiler-service.ts} (62%) diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index ca8fe39888..ad181a1062 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -414,8 +414,8 @@ injector.require( injector.requirePublic("cleanupService", "./services/cleanup-service"); injector.require( - "webpackCompilerService", - "./services/webpack/webpack-compiler-service", + "bundlerCompilerService", + "./services/bundler/bundler-compiler-service", ); injector.require( diff --git a/lib/constants.ts b/lib/constants.ts index 0ac436ec77..b9dc247d83 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -16,6 +16,7 @@ export const SCOPED_TNS_CORE_MODULES = "@nativescript/core"; export const TNS_CORE_THEME_NAME = "nativescript-theme-core"; export const SCOPED_TNS_CORE_THEME_NAME = "@nativescript/theme"; export const WEBPACK_PLUGIN_NAME = "@nativescript/webpack"; +export const RSPACK_PLUGIN_NAME = "@nativescript/rspack"; export const TNS_CORE_MODULES_WIDGETS_NAME = "tns-core-modules-widgets"; export const UI_MOBILE_BASE_NAME = "@nativescript/ui-mobile-base"; export const TNS_ANDROID_RUNTIME_NAME = "tns-android"; @@ -36,6 +37,7 @@ export const XML_FILE_EXTENSION = ".xml"; export const PLATFORMS_DIR_NAME = "platforms"; export const HOOKS_DIR_NAME = "hooks"; export const WEBPACK_CONFIG_NAME = "webpack.config.js"; +export const RSPACK_CONFIG_NAME = "rspack.config.js"; export const TSCCONFIG_TNS_JSON_NAME = "tsconfig.tns.json"; export const KARMA_CONFIG_NAME = "karma.conf.js"; export const LIB_DIR_NAME = "lib"; @@ -223,6 +225,7 @@ export const FILES_CHANGE_EVENT_NAME = "filesChangeEvent"; export const INITIAL_SYNC_EVENT_NAME = "initialSyncEvent"; export const PREPARE_READY_EVENT_NAME = "prepareReadyEvent"; export const WEBPACK_COMPILATION_COMPLETE = "webpackCompilationComplete"; +export const BUNDLER_COMPILATION_COMPLETE = "bundlerCompilationComplete"; export class DebugCommandErrors { public static UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR = diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index 8ba3550b86..7b21219f63 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -67,7 +67,7 @@ export class PrepareController extends EventEmitter { private $prepareNativePlatformService: IPrepareNativePlatformService, private $projectChangesService: IProjectChangesService, private $projectDataService: IProjectDataService, - private $webpackCompilerService: IWebpackCompilerService, + private $bundlerCompilerService: IBundlerCompilerService, private $watchIgnoreListService: IWatchIgnoreListService, private $analyticsService: IAnalyticsService, private $markingModeService: IMarkingModeService, @@ -117,8 +117,8 @@ export class PrepareController extends EventEmitter { this.watchersData[projectDir][platformLowerCase] && this.watchersData[projectDir][platformLowerCase].hasWebpackCompilerProcess ) { - await this.$webpackCompilerService.stopWebpackCompiler(platformLowerCase); - this.$webpackCompilerService.removeListener( + await this.$bundlerCompilerService.stopBundlerCompiler(platformLowerCase); + this.$bundlerCompilerService.removeListener( WEBPACK_COMPILATION_COMPLETE, this.webpackCompilerHandler, ); @@ -177,7 +177,7 @@ export class PrepareController extends EventEmitter { prepareData, ); } else { - await this.$webpackCompilerService.compileWithoutWatch( + await this.$bundlerCompilerService.compileWithoutWatch( platformData, projectData, prepareData, @@ -296,7 +296,7 @@ export class PrepareController extends EventEmitter { }; this.webpackCompilerHandler = handler.bind(this); - this.$webpackCompilerService.on( + this.$bundlerCompilerService.on( WEBPACK_COMPILATION_COMPLETE, this.webpackCompilerHandler, ); @@ -304,7 +304,7 @@ export class PrepareController extends EventEmitter { this.watchersData[projectData.projectDir][ platformData.platformNameLowerCase ].hasWebpackCompilerProcess = true; - await this.$webpackCompilerService.compileWithWatch( + await this.$bundlerCompilerService.compileWithWatch( platformData, projectData, prepareData, @@ -560,7 +560,7 @@ export class PrepareController extends EventEmitter { if (this.pausedFileWatch) { for (const watcher of watchers) { for (const platform in watcher) { - await this.$webpackCompilerService.stopWebpackCompiler(platform); + await this.$bundlerCompilerService.stopBundlerCompiler(platform); watcher[platform].hasWebpackCompilerProcess = false; } } @@ -569,7 +569,7 @@ export class PrepareController extends EventEmitter { for (const platform in watcher) { const args = watcher[platform].prepareArguments; watcher[platform].hasWebpackCompilerProcess = true; - await this.$webpackCompilerService.compileWithWatch( + await this.$bundlerCompilerService.compileWithWatch( args.platformData, args.projectData, args.prepareData, diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 3b557bf5e1..c8046b2ed4 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -121,6 +121,7 @@ export interface IOSLocalSPMPackage extends IOSSPMPackageBase { } export type IOSSPMPackage = IOSRemoteSPMPackage | IOSLocalSPMPackage; +export type BundlerType = "webpack" | "rspack" | "vite"; interface INsConfigIOS extends INsConfigPlaform { discardUncaughtJsExceptions?: boolean; @@ -183,6 +184,7 @@ interface INsConfig { overridePods?: string; webpackConfigPath?: string; bundlerConfigPath?: string; + bundler?: BundlerType; ios?: INsConfigIOS; android?: INsConfigAndroid; visionos?: INSConfigVisionOS; @@ -216,7 +218,16 @@ interface IProjectData extends ICreateProjectData { * Value is true when project has nativescript.config and it has `shared: true` in it. */ isShared: boolean; - + /** + * Specifies the bundler used to build the application. + * + * - `"webpack"`: Uses Webpack for traditional bundling. + * - `"rspack"`: Uses Rspack for fast bundling. + * - `"vite"`: Uses Vite for fast bundling. + * + * @default "webpack" + */ + bundler: BundlerType; /** * @deprecated Use bundlerConfigPath * Defines the path to the configuration file passed to webpack process. diff --git a/lib/project-data.ts b/lib/project-data.ts index 5505ca8b6e..cbe06da65f 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -5,6 +5,7 @@ import { parseJson } from "./common/helpers"; import { EOL } from "os"; import { cache } from "./common/decorators"; import { + BundlerType, INsConfig, IProjectConfigService, IProjectData, @@ -100,6 +101,7 @@ export class ProjectData implements IProjectData { public isShared: boolean; public webpackConfigPath: string; public bundlerConfigPath: string; + public bundler: BundlerType; public initialized: boolean; constructor( @@ -213,6 +215,13 @@ export class ProjectData implements IProjectData { this.nsConfig && this.nsConfig.bundlerConfigPath ? path.resolve(this.projectDir, this.nsConfig.bundlerConfigPath) : null; + this.bundler = + this.nsConfig && this.nsConfig.bundler + ? (path.resolve( + this.projectDir, + this.nsConfig.bundler, + ) as BundlerType) + : "webpack"; return; } diff --git a/lib/services/webpack/webpack-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts similarity index 77% rename from lib/services/webpack/webpack-compiler-service.ts rename to lib/services/bundler/bundler-compiler-service.ts index 0c8924b2a2..65b496a2b3 100644 --- a/lib/services/webpack/webpack-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -5,8 +5,8 @@ import * as _ from "lodash"; import { EventEmitter } from "events"; import { performanceLog } from "../../common/decorators"; import { - WEBPACK_COMPILATION_COMPLETE, WEBPACK_PLUGIN_NAME, + BUNDLER_COMPILATION_COMPLETE, PackageManagers, CONFIG_FILE_NAME_DISPLAY, } from "../../constants"; @@ -16,7 +16,11 @@ import { IOptions, } from "../../declarations"; import { IPlatformData } from "../../definitions/platform"; -import { IProjectConfigService, IProjectData } from "../../definitions/project"; +import { + BundlerType, + IProjectConfigService, + IProjectData, +} from "../../definitions/project"; import { IDictionary, IErrors, @@ -34,23 +38,23 @@ import { } from "../../helpers/package-path-helper"; // todo: move out of here -interface IWebpackMessage { +interface IBundlerMessage { type: "compilation" | "hmr-status"; version?: number; hash?: string; data?: T; } -interface IWebpackCompilation { +interface IBundlerCompilation { emittedAssets: string[]; staleAssets: string[]; } -export class WebpackCompilerService +export class BundlerCompilerService extends EventEmitter - implements IWebpackCompilerService + implements IBundlerCompilerService { - private webpackProcesses: IDictionary = {}; + private bundlerProcesses: IDictionary = {}; private expectedHashes: IStringDictionary = {}; constructor( @@ -76,15 +80,15 @@ export class WebpackCompilerService prepareData: IPrepareData, ): Promise { return new Promise(async (resolve, reject) => { - if (this.webpackProcesses[platformData.platformNameLowerCase]) { + if (this.bundlerProcesses[platformData.platformNameLowerCase]) { resolve(void 0); return; } - let isFirstWebpackWatchCompilation = true; + let isFirstBundlerWatchCompilation = true; prepareData.watch = true; try { - const childProcess = await this.startWebpackProcess( + const childProcess = await this.startBundleProcess( platformData, projectData, prepareData, @@ -98,8 +102,8 @@ export class WebpackCompilerService process.stderr.write(data); }); - childProcess.on("message", (message: string | IWebpackEmitMessage) => { - this.$logger.trace("Message from webpack", message); + childProcess.on("message", (message: string | IBundlerEmitMessage) => { + this.$logger.trace(`Message from ${projectData.bundler}`, message); // if we are on webpack5 - we handle HMR in a slightly different way if ( @@ -109,8 +113,8 @@ export class WebpackCompilerService ) { // first compilation can be ignored because it will be synced regardless // handling it here would trigger 2 syncs - if (isFirstWebpackWatchCompilation) { - isFirstWebpackWatchCompilation = false; + if (isFirstBundlerWatchCompilation) { + isFirstBundlerWatchCompilation = false; resolve(childProcess); return; } @@ -123,22 +127,27 @@ export class WebpackCompilerService // } return this.handleHMRMessage( - message as IWebpackMessage, + message as IBundlerMessage, platformData, projectData, prepareData, ); } - if (message === "Webpack compilation complete.") { - this.$logger.info("Webpack build done!"); + if ( + message === + `${capitalizeFirstLetter(projectData.bundler)} compilation complete.` + ) { + this.$logger.info( + `${capitalizeFirstLetter(projectData.bundler)} build done!`, + ); resolve(childProcess); } - message = message as IWebpackEmitMessage; + message = message as IBundlerEmitMessage; if (message.emittedFiles) { - if (isFirstWebpackWatchCompilation) { - isFirstWebpackWatchCompilation = false; + if (isFirstBundlerWatchCompilation) { + isFirstBundlerWatchCompilation = false; this.expectedHashes[platformData.platformNameLowerCase] = prepareData.hmr ? message.hash : ""; return; @@ -190,7 +199,10 @@ export class WebpackCompilerService platform: platformData.platformNameLowerCase, }; - this.$logger.trace("Generated data from webpack message:", data); + this.$logger.trace( + `Generated data from ${projectData.bundler} 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) { @@ -198,16 +210,16 @@ export class WebpackCompilerService } if (data.files.length) { - this.emit(WEBPACK_COMPILATION_COMPLETE, data); + this.emit(BUNDLER_COMPILATION_COMPLETE, data); } } }); childProcess.on("error", (err) => { this.$logger.trace( - `Unable to start webpack process in watch mode. Error is: ${err}`, + `Unable to start ${projectData.bundler} process in watch mode. Error is: ${err}`, ); - delete this.webpackProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[platformData.platformNameLowerCase]; reject(err); }); @@ -218,13 +230,13 @@ export class WebpackCompilerService 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.`, + `${capitalizeFirstLetter(projectData.bundler)} process exited with code ${exitCode} when we expected it to be long living with watch.`, ); const error: any = new Error( - `Executing webpack failed with exit code ${exitCode}.`, + `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, ); error.code = exitCode; - delete this.webpackProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[platformData.platformNameLowerCase]; reject(error); }); } catch (err) { @@ -239,13 +251,13 @@ export class WebpackCompilerService prepareData: IPrepareData, ): Promise { return new Promise(async (resolve, reject) => { - if (this.webpackProcesses[platformData.platformNameLowerCase]) { + if (this.bundlerProcesses[platformData.platformNameLowerCase]) { resolve(); return; } try { - const childProcess = await this.startWebpackProcess( + const childProcess = await this.startBundleProcess( platformData, projectData, prepareData, @@ -253,9 +265,9 @@ export class WebpackCompilerService childProcess.on("error", (err) => { this.$logger.trace( - `Unable to start webpack process in non-watch mode. Error is: ${err}`, + `Unable to start ${projectData.bundler} process in non-watch mode. Error is: ${err}`, ); - delete this.webpackProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[platformData.platformNameLowerCase]; reject(err); }); @@ -264,13 +276,13 @@ export class WebpackCompilerService childProcess.pid.toString(), ); - delete this.webpackProcesses[platformData.platformNameLowerCase]; + delete this.bundlerProcesses[platformData.platformNameLowerCase]; const exitCode = typeof arg === "number" ? arg : arg && arg.code; if (exitCode === 0) { resolve(); } else { const error: any = new Error( - `Executing webpack failed with exit code ${exitCode}.`, + `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, ); error.code = exitCode; reject(error); @@ -282,14 +294,14 @@ export class WebpackCompilerService }); } - public async stopWebpackCompiler(platform: string): Promise { + public async stopBundlerCompiler(platform: string): Promise { if (platform) { - await this.stopWebpackForPlatform(platform); + await this.stopBundlerForPlatform(platform); } else { - const webpackedPlatforms = Object.keys(this.webpackProcesses); + const bundlerPlatforms = Object.keys(this.bundlerProcesses); - for (let i = 0; i < webpackedPlatforms.length; i++) { - await this.stopWebpackForPlatform(webpackedPlatforms[i]); + for (let i = 0; i < bundlerPlatforms.length; i++) { + await this.stopBundlerForPlatform(bundlerPlatforms[i]); } } } @@ -305,7 +317,7 @@ export class WebpackCompilerService } @performanceLog() - private async startWebpackProcess( + private async startBundleProcess( platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData, @@ -317,9 +329,9 @@ export class WebpackCompilerService ); } } else { - if (!this.$fs.exists(projectData.webpackConfigPath)) { + if (!this.$fs.exists(projectData.bundlerConfigPath)) { this.$errors.fail( - `The webpack configuration file ${projectData.webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}.`, + `The ${projectData.bundler} configuration file ${projectData.bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}.`, ); } } @@ -348,9 +360,9 @@ export class WebpackCompilerService const args = [ ...additionalNodeArgs, - this.getWebpackExecutablePath(projectData), + this.getBundlerExecutablePath(projectData), this.isModernBundler(projectData) ? `build` : null, - `--config=${projectData.bundlerConfigPath || projectData.webpackConfigPath}`, + `--config=${projectData.bundlerConfigPath}`, ...envParams, ].filter(Boolean); @@ -365,6 +377,7 @@ export class WebpackCompilerService }; options.env = { NATIVESCRIPT_WEBPACK_ENV: JSON.stringify(envData), + NATIVESCRIPT_BUNDLER_ENV: JSON.stringify(envData), }; if (this.$hostInfo.isWindows) { Object.assign(options.env, { APPDATA: process.env.appData }); @@ -384,7 +397,7 @@ export class WebpackCompilerService options, ); - this.webpackProcesses[platformData.platformNameLowerCase] = childProcess; + this.bundlerProcesses[platformData.platformNameLowerCase] = childProcess; await this.$cleanupService.addKillProcess(childProcess.pid.toString()); return childProcess; @@ -475,23 +488,26 @@ export class WebpackCompilerService ); envFlagNames.splice(envFlagNames.indexOf("snapshot"), 1); } else if (this.$hostInfo.isWindows) { - const minWebpackPluginWithWinSnapshotsVersion = "1.3.0"; - const installedWebpackPluginVersion = - await this.$packageInstallationManager.getInstalledDependencyVersion( - 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 ${WEBPACK_PLUGIN_NAME}@latest).`, - ); + if (projectData.bundler === "webpack") { + //TODO: check this use case for webpack5 WEBPACK_PLUGIN_NAME + const minWebpackPluginWithWinSnapshotsVersion = "1.3.0"; + const installedWebpackPluginVersion = + await this.$packageInstallationManager.getInstalledDependencyVersion( + 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 ${WEBPACK_PLUGIN_NAME}@latest).`, + ); + } } } } @@ -570,30 +586,37 @@ export class WebpackCompilerService return hotHash || ""; } - private async stopWebpackForPlatform(platform: string) { - 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]; + private async stopBundlerForPlatform(platform: string) { + this.$logger.trace( + `Stopping ${this.getBundler()} watch for platform ${platform}.`, + ); + const bundlerProcess = this.bundlerProcesses[platform]; + await this.$cleanupService.removeKillProcess(bundlerProcess.pid.toString()); + if (bundlerProcess) { + bundlerProcess.kill("SIGINT"); + delete this.bundlerProcesses[platform]; } } private handleHMRMessage( - message: IWebpackMessage, + message: IBundlerMessage, platformData: IPlatformData, projectData: IProjectData, prepareData: IPrepareData, ) { - // handle new webpack hmr packets - this.$logger.trace("Received message from webpack process:", message); + // handle new bundler hmr packets + this.$logger.trace( + `Received message from ${projectData.bundler} process:`, + message, + ); if (message.type !== "compilation") { return; } - this.$logger.trace("Webpack build done!"); + this.$logger.trace( + `${capitalizeFirstLetter(projectData.bundler)} build done!`, + ); const files = message.data.emittedAssets.map((asset: string) => path.join( @@ -632,7 +655,7 @@ export class WebpackCompilerService return; } - this.emit(WEBPACK_COMPILATION_COMPLETE, { + this.emit(BUNDLER_COMPILATION_COMPLETE, { files, staleFiles, hasOnlyHotUpdateFiles: prepareData.hmr, @@ -644,7 +667,7 @@ export class WebpackCompilerService }); } - private getWebpackExecutablePath(projectData: IProjectData): string { + private getBundlerExecutablePath(projectData: IProjectData): string { const bundler = this.getBundler(); if (this.isModernBundler(projectData)) { @@ -674,12 +697,9 @@ export class WebpackCompilerService case "rspack": return true; default: - const packageJSONPath = resolvePackageJSONPath( - "@nativescript/webpack", - { - paths: [projectData.projectDir], - }, - ); + const packageJSONPath = resolvePackageJSONPath(WEBPACK_PLUGIN_NAME, { + paths: [projectData.projectDir], + }); if (packageJSONPath) { const packageData = this.$fs.readJson(packageJSONPath); @@ -695,9 +715,13 @@ export class WebpackCompilerService return false; } - public getBundler(): "webpack" | "rspack" | "vite" { + public getBundler(): BundlerType { return this.$projectConfigService.getValue(`bundler`, "webpack"); } } -injector.register("webpackCompilerService", WebpackCompilerService); +function capitalizeFirstLetter(val: string) { + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +} + +injector.register("bundlerCompilerService", BundlerCompilerService); diff --git a/lib/services/webpack/webpack.d.ts b/lib/services/bundler/bundler.ts similarity index 90% rename from lib/services/webpack/webpack.d.ts rename to lib/services/bundler/bundler.ts index e3a8dddd8b..691d45f8c5 100644 --- a/lib/services/webpack/webpack.d.ts +++ b/lib/services/bundler/bundler.ts @@ -18,21 +18,21 @@ import { import { INotConfiguredEnvOptions } from "../../common/definitions/commands"; declare global { - interface IWebpackCompilerService extends EventEmitter { + interface IBundlerCompilerService extends EventEmitter { compileWithWatch( platformData: IPlatformData, projectData: IProjectData, - prepareData: IPrepareData + prepareData: IPrepareData, ): Promise; compileWithoutWatch( platformData: IPlatformData, projectData: IProjectData, - prepareData: IPrepareData + prepareData: IPrepareData, ): Promise; - stopWebpackCompiler(platform: string): Promise; + stopBundlerCompiler(platform: string): Promise; } - interface IWebpackEnvOptions { + interface IBundlerEnvOptions { sourceMap?: boolean; uglify?: boolean; production?: boolean; @@ -42,19 +42,19 @@ declare global { checkForChanges( platformData: IPlatformData, projectData: IProjectData, - prepareData: IPrepareData + prepareData: IPrepareData, ): Promise; getPrepareInfoFilePath(platformData: IPlatformData): string; getPrepareInfo(platformData: IPlatformData): IPrepareInfo; savePrepareInfo( platformData: IPlatformData, projectData: IProjectData, - prepareData: IPrepareData + prepareData: IPrepareData, ): Promise; setNativePlatformStatus( platformData: IPlatformData, projectData: IProjectData, - addedPlatform: IAddedNativePlatform + addedPlatform: IAddedNativePlatform, ): void; currentChanges: IProjectChangesInfo; } @@ -68,7 +68,7 @@ declare global { hasNativeChanges: boolean; } - interface IWebpackEmitMessage { + interface IBundlerEmitMessage { emittedFiles: string[]; chunkFiles: string[]; hash: string; @@ -81,12 +81,12 @@ declare global { validate( projectData: IProjectData, options: IOptions, - notConfiguredEnvOptions?: INotConfiguredEnvOptions + notConfiguredEnvOptions?: INotConfiguredEnvOptions, ): Promise; createProject( frameworkDir: string, frameworkVersion: string, - projectData: IProjectData + projectData: IProjectData, ): Promise; interpolateData(projectData: IProjectData): Promise; interpolateConfigurationFile(projectData: IProjectData): void; @@ -108,13 +108,13 @@ declare global { validateOptions( projectId?: string, provision?: true | string, - teamId?: true | string + teamId?: true | string, ): Promise; buildProject( projectRoot: string, projectData: IProjectData, - buildConfig: T + buildConfig: T, ): Promise; /** @@ -125,7 +125,7 @@ declare global { */ prepareProject( projectData: IProjectData, - prepareData: T + prepareData: T, ): Promise; /** @@ -146,7 +146,7 @@ declare global { preparePluginNativeCode( pluginData: IPluginData, - options?: any + options?: any, ): Promise; /** @@ -157,17 +157,17 @@ declare global { */ removePluginNativeCode( pluginData: IPluginData, - projectData: IProjectData + projectData: IProjectData, ): Promise; beforePrepareAllPlugins( projectData: IProjectData, - dependencies?: IDependencyData[] + dependencies?: IDependencyData[], ): Promise; handleNativeDependenciesChange( projectData: IProjectData, - opts: IRelease + opts: IRelease, ): Promise; /** @@ -178,11 +178,11 @@ declare global { cleanDeviceTempFolder( deviceIdentifier: string, - projectData: IProjectData + projectData: IProjectData, ): Promise; processConfigurationFilesFromAppResources( projectData: IProjectData, - opts: { release: boolean } + opts: { release: boolean }, ): Promise; /** @@ -214,7 +214,7 @@ declare global { checkForChanges( changeset: IProjectChangesInfo, prepareData: T, - projectData: IProjectData + projectData: IProjectData, ): Promise; /** diff --git a/lib/services/bundler_Asdf/bundler-compiler-service.ts b/lib/services/bundler_Asdf/bundler-compiler-service.ts new file mode 100644 index 0000000000..65b496a2b3 --- /dev/null +++ b/lib/services/bundler_Asdf/bundler-compiler-service.ts @@ -0,0 +1,727 @@ +import * as path from "path"; +import * as child_process from "child_process"; +import * as semver from "semver"; +import * as _ from "lodash"; +import { EventEmitter } from "events"; +import { performanceLog } from "../../common/decorators"; +import { + WEBPACK_PLUGIN_NAME, + BUNDLER_COMPILATION_COMPLETE, + PackageManagers, + CONFIG_FILE_NAME_DISPLAY, +} from "../../constants"; +import { + IPackageManager, + IPackageInstallationManager, + IOptions, +} from "../../declarations"; +import { IPlatformData } from "../../definitions/platform"; +import { + BundlerType, + IProjectConfigService, + IProjectData, +} from "../../definitions/project"; +import { + IDictionary, + IErrors, + IStringDictionary, + IChildProcess, + IFileSystem, + IHooksService, + IHostInfo, +} from "../../common/declarations"; +import { ICleanupService } from "../../definitions/cleanup-service"; +import { injector } from "../../common/yok"; +import { + resolvePackagePath, + resolvePackageJSONPath, +} from "../../helpers/package-path-helper"; + +// todo: move out of here +interface IBundlerMessage { + type: "compilation" | "hmr-status"; + version?: number; + hash?: string; + data?: T; +} + +interface IBundlerCompilation { + emittedAssets: string[]; + staleAssets: string[]; +} + +export class BundlerCompilerService + extends EventEmitter + implements IBundlerCompilerService +{ + private bundlerProcesses: IDictionary = {}; + private expectedHashes: IStringDictionary = {}; + + constructor( + private $options: IOptions, + private $errors: IErrors, + private $childProcess: IChildProcess, + public $fs: IFileSystem, + public $hooksService: IHooksService, + public $hostInfo: IHostInfo, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $cleanupService: ICleanupService, + private $packageManager: IPackageManager, + private $packageInstallationManager: IPackageInstallationManager, + private $projectConfigService: IProjectConfigService, + ) { + super(); + } + + public async compileWithWatch( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise { + return new Promise(async (resolve, reject) => { + if (this.bundlerProcesses[platformData.platformNameLowerCase]) { + resolve(void 0); + return; + } + + let isFirstBundlerWatchCompilation = true; + prepareData.watch = true; + try { + const childProcess = await this.startBundleProcess( + 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: string | IBundlerEmitMessage) => { + this.$logger.trace(`Message from ${projectData.bundler}`, 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 (isFirstBundlerWatchCompilation) { + isFirstBundlerWatchCompilation = 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 as IBundlerMessage, + platformData, + projectData, + prepareData, + ); + } + + if ( + message === + `${capitalizeFirstLetter(projectData.bundler)} compilation complete.` + ) { + this.$logger.info( + `${capitalizeFirstLetter(projectData.bundler)} build done!`, + ); + resolve(childProcess); + } + + message = message as IBundlerEmitMessage; + if (message.emittedFiles) { + if (isFirstBundlerWatchCompilation) { + isFirstBundlerWatchCompilation = 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: string) => + path.join( + platformData.appDestinationDirectoryPath, + this.$options.hostProjectModuleName, + file, + ), + ); + const fallbackFiles = result.fallbackFiles.map((file: string) => + 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 ${projectData.bundler} 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(BUNDLER_COMPILATION_COMPLETE, data); + } + } + }); + + childProcess.on("error", (err) => { + this.$logger.trace( + `Unable to start ${projectData.bundler} process in watch mode. Error is: ${err}`, + ); + delete this.bundlerProcesses[platformData.platformNameLowerCase]; + reject(err); + }); + + childProcess.on("close", async (arg: any) => { + await this.$cleanupService.removeKillProcess( + childProcess.pid.toString(), + ); + + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + this.$logger.trace( + `${capitalizeFirstLetter(projectData.bundler)} process exited with code ${exitCode} when we expected it to be long living with watch.`, + ); + const error: any = new Error( + `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, + ); + error.code = exitCode; + delete this.bundlerProcesses[platformData.platformNameLowerCase]; + reject(error); + }); + } catch (err) { + reject(err); + } + }); + } + + public async compileWithoutWatch( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise { + return new Promise(async (resolve, reject) => { + if (this.bundlerProcesses[platformData.platformNameLowerCase]) { + resolve(); + return; + } + + try { + const childProcess = await this.startBundleProcess( + platformData, + projectData, + prepareData, + ); + + childProcess.on("error", (err) => { + this.$logger.trace( + `Unable to start ${projectData.bundler} process in non-watch mode. Error is: ${err}`, + ); + delete this.bundlerProcesses[platformData.platformNameLowerCase]; + reject(err); + }); + + childProcess.on("close", async (arg: any) => { + await this.$cleanupService.removeKillProcess( + childProcess.pid.toString(), + ); + + delete this.bundlerProcesses[platformData.platformNameLowerCase]; + const exitCode = typeof arg === "number" ? arg : arg && arg.code; + if (exitCode === 0) { + resolve(); + } else { + const error: any = new Error( + `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, + ); + error.code = exitCode; + reject(error); + } + }); + } catch (err) { + reject(err); + } + }); + } + + public async stopBundlerCompiler(platform: string): Promise { + if (platform) { + await this.stopBundlerForPlatform(platform); + } else { + const bundlerPlatforms = Object.keys(this.bundlerProcesses); + + for (let i = 0; i < bundlerPlatforms.length; i++) { + await this.stopBundlerForPlatform(bundlerPlatforms[i]); + } + } + } + + private async shouldUsePreserveSymlinksOption(): Promise { + // 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 !== PackageManagers.pnpm; + return res; + } + + @performanceLog() + private async startBundleProcess( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise { + if (projectData.bundlerConfigPath) { + if (!this.$fs.exists(projectData.bundlerConfigPath)) { + this.$errors.fail( + `The bundler configuration file ${projectData.bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}.`, + ); + } + } else { + if (!this.$fs.exists(projectData.bundlerConfigPath)) { + this.$errors.fail( + `The ${projectData.bundler} configuration file ${projectData.bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${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.getBundlerExecutablePath(projectData), + this.isModernBundler(projectData) ? `build` : null, + `--config=${projectData.bundlerConfigPath}`, + ...envParams, + ].filter(Boolean); + + if (prepareData.watch) { + args.push("--watch"); + } + + const stdio = prepareData.watch ? ["ipc"] : "inherit"; + const options: { [key: string]: any } = { + cwd: projectData.projectDir, + stdio, + }; + options.env = { + NATIVESCRIPT_WEBPACK_ENV: JSON.stringify(envData), + NATIVESCRIPT_BUNDLER_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.bundlerProcesses[platformData.platformNameLowerCase] = childProcess; + await this.$cleanupService.addKillProcess(childProcess.pid.toString()); + + return childProcess; + } + + private buildEnvData( + platform: string, + projectData: IProjectData, + prepareData: IPrepareData, + ) { + 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 = + process.env.NATIVESCRIPT_CONFIG_NAME ?? this.$options.config ?? "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 prepareData?.env?.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; + } + + private async buildEnvCommandLineParams( + envData: any, + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ) { + 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) { + if (projectData.bundler === "webpack") { + //TODO: check this use case for webpack5 WEBPACK_PLUGIN_NAME + const minWebpackPluginWithWinSnapshotsVersion = "1.3.0"; + const installedWebpackPluginVersion = + await this.$packageInstallationManager.getInstalledDependencyVersion( + 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 ${WEBPACK_PLUGIN_NAME}@latest).`, + ); + } + } + } + } + + const args: any[] = []; + 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: any) => args.push(`--env.${item}=${value}`)); + } + }); + + return args; + } + + public getUpdatedEmittedFiles( + allEmittedFiles: string[], + chunkFiles: string[], + nextHash: string, + platform: string, + ) { + 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, + }; + } + + private getCurrentHotUpdateHash(emittedFiles: string[]) { + 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 || ""; + } + + private async stopBundlerForPlatform(platform: string) { + this.$logger.trace( + `Stopping ${this.getBundler()} watch for platform ${platform}.`, + ); + const bundlerProcess = this.bundlerProcesses[platform]; + await this.$cleanupService.removeKillProcess(bundlerProcess.pid.toString()); + if (bundlerProcess) { + bundlerProcess.kill("SIGINT"); + delete this.bundlerProcesses[platform]; + } + } + + private handleHMRMessage( + message: IBundlerMessage, + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ) { + // handle new bundler hmr packets + this.$logger.trace( + `Received message from ${projectData.bundler} process:`, + message, + ); + + if (message.type !== "compilation") { + return; + } + + this.$logger.trace( + `${capitalizeFirstLetter(projectData.bundler)} build done!`, + ); + + const files = message.data.emittedAssets.map((asset: string) => + path.join( + platformData.appDestinationDirectoryPath, + this.$options.hostProjectModuleName, + asset, + ), + ); + const staleFiles = message.data.staleAssets.map((asset: string) => + path.join( + platformData.appDestinationDirectoryPath, + this.$options.hostProjectModuleName, + asset, + ), + ); + + // extract last hash from emitted filenames + const lastHash = (() => { + const absoluteFileNameWithLastHash = files.find((fileName: string) => + 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(BUNDLER_COMPILATION_COMPLETE, { + files, + staleFiles, + hasOnlyHotUpdateFiles: prepareData.hmr, + hmrData: { + hash: lastHash || message.hash, + fallbackFiles: [], + }, + platform: platformData.platformNameLowerCase, + }); + } + + private getBundlerExecutablePath(projectData: IProjectData): string { + const bundler = this.getBundler(); + + if (this.isModernBundler(projectData)) { + const packagePath = resolvePackagePath(`@nativescript/${bundler}`, { + paths: [projectData.projectDir], + }); + + if (packagePath) { + return path.resolve(packagePath, "dist", "bin", "index.js"); + } + } + + const packagePath = resolvePackagePath("webpack", { + paths: [projectData.projectDir], + }); + + if (!packagePath) { + return ""; + } + + return path.resolve(packagePath, "bin", "webpack.js"); + } + + private isModernBundler(projectData: IProjectData): boolean { + const bundler = this.getBundler(); + switch (bundler) { + case "rspack": + return true; + default: + const packageJSONPath = resolvePackageJSONPath(WEBPACK_PLUGIN_NAME, { + 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; + } + } + break; + } + + return false; + } + + public getBundler(): BundlerType { + return this.$projectConfigService.getValue(`bundler`, "webpack"); + } +} + +function capitalizeFirstLetter(val: string) { + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +} + +injector.register("bundlerCompilerService", BundlerCompilerService); diff --git a/lib/services/bundler_Asdf/bundler.d.ts b/lib/services/bundler_Asdf/bundler.d.ts new file mode 100644 index 0000000000..691d45f8c5 --- /dev/null +++ b/lib/services/bundler_Asdf/bundler.d.ts @@ -0,0 +1,226 @@ +import { EventEmitter } from "events"; +import { BuildData } from "../../data/build-data"; +import { PrepareData } from "../../data/prepare-data"; +import { + IPlatformProjectServiceBase, + IProjectData, + IValidatePlatformOutput, +} from "../../definitions/project"; +import { IOptions, IDependencyData } from "../../declarations"; +import { IPlatformData } from "../../definitions/platform"; +import { IPluginData } from "../../definitions/plugins"; +import { IRelease, ISpawnResult } from "../../common/declarations"; +import { + IProjectChangesInfo, + IPrepareInfo, + IAddedNativePlatform, +} from "../../definitions/project-changes"; +import { INotConfiguredEnvOptions } from "../../common/definitions/commands"; + +declare global { + interface IBundlerCompilerService extends EventEmitter { + compileWithWatch( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise; + compileWithoutWatch( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise; + stopBundlerCompiler(platform: string): Promise; + } + + interface IBundlerEnvOptions { + sourceMap?: boolean; + uglify?: boolean; + production?: boolean; + } + + interface IProjectChangesService { + checkForChanges( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise; + getPrepareInfoFilePath(platformData: IPlatformData): string; + getPrepareInfo(platformData: IPlatformData): IPrepareInfo; + savePrepareInfo( + platformData: IPlatformData, + projectData: IProjectData, + prepareData: IPrepareData, + ): Promise; + setNativePlatformStatus( + platformData: IPlatformData, + projectData: IProjectData, + addedPlatform: IAddedNativePlatform, + ): void; + currentChanges: IProjectChangesInfo; + } + + interface IFilesChangeEventData { + platform: string; + files: string[]; + staleFiles: string[]; + hmrData: IPlatformHmrData; + hasOnlyHotUpdateFiles: boolean; + hasNativeChanges: boolean; + } + + interface IBundlerEmitMessage { + emittedFiles: string[]; + chunkFiles: string[]; + hash: string; + } + + interface IPlatformProjectService + extends NodeJS.EventEmitter, + IPlatformProjectServiceBase { + getPlatformData(projectData: IProjectData): IPlatformData; + validate( + projectData: IProjectData, + options: IOptions, + notConfiguredEnvOptions?: INotConfiguredEnvOptions, + ): Promise; + createProject( + frameworkDir: string, + frameworkVersion: string, + projectData: IProjectData, + ): Promise; + interpolateData(projectData: IProjectData): Promise; + interpolateConfigurationFile(projectData: IProjectData): void; + + /** + * Executes additional actions after native project is created. + * @param {string} projectRoot Path to the real NativeScript project. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + afterCreateProject(projectRoot: string, projectData: IProjectData): void; + + /** + * Gets first chance to validate the options provided as command line arguments. + * @param {string} projectId Project identifier - for example org.nativescript.test. + * @param {any} provision UUID of the provisioning profile used in iOS option validation. + * @returns {void} + */ + validateOptions( + projectId?: string, + provision?: true | string, + teamId?: true | string, + ): Promise; + + buildProject( + projectRoot: string, + projectData: IProjectData, + buildConfig: T, + ): Promise; + + /** + * Prepares images in Native project (for iOS). + * @param {IProjectData} projectData DTO with information about the project. + * @param {any} platformSpecificData Platform specific data required for project preparation. + * @returns {void} + */ + prepareProject( + projectData: IProjectData, + prepareData: T, + ): Promise; + + /** + * Prepares App_Resources in the native project by clearing data from other platform and applying platform specific rules. + * @param {string} appResourcesDirectoryPath The place in the native project where the App_Resources are copied first. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + prepareAppResources(projectData: IProjectData): void; + + /** + * Defines if current platform is prepared (i.e. if /platforms/ dir exists). + * @param {string} projectRoot The project directory (path where root's package.json is located). + * @param {IProjectData} projectData DTO with information about the project. + * @returns {boolean} True in case platform is prepare (i.e. if /platforms/ dir exists), false otherwise. + */ + isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean; + + preparePluginNativeCode( + pluginData: IPluginData, + options?: any, + ): Promise; + + /** + * Removes native code of a plugin (CocoaPods, jars, libs, src). + * @param {IPluginData} Plugins data describing the plugin which should be cleaned. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + removePluginNativeCode( + pluginData: IPluginData, + projectData: IProjectData, + ): Promise; + + beforePrepareAllPlugins( + projectData: IProjectData, + dependencies?: IDependencyData[], + ): Promise; + + handleNativeDependenciesChange( + projectData: IProjectData, + opts: IRelease, + ): Promise; + + /** + * Gets the path wheren App_Resources should be copied. + * @returns {string} Path to native project, where App_Resources should be copied. + */ + getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string; + + cleanDeviceTempFolder( + deviceIdentifier: string, + projectData: IProjectData, + ): Promise; + processConfigurationFilesFromAppResources( + projectData: IProjectData, + opts: { release: boolean }, + ): Promise; + + /** + * Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources. + * @param {IProjectData} projectData DTO with information about the project. + * @returns {void} + */ + ensureConfigurationFileInAppResources(projectData: IProjectData): void; + + /** + * Stops all running processes that might hold a lock on the filesystem. + * Android: Gradle daemon processes are terminated. + * @param {IPlatformData} platformData The data for the specified platform. + * @returns {void} + */ + stopServices?(projectRoot: string): Promise; + + /** + * Removes build artifacts specific to the platform + * @param {string} projectRoot The root directory of the native project. + * @returns {void} + */ + cleanProject?(projectRoot: string): Promise; + + /** + * Check the current state of the project, and validate against the options. + * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. + */ + checkForChanges( + changeset: IProjectChangesInfo, + prepareData: T, + projectData: IProjectData, + ): Promise; + + /** + * Get the deployment target's version + * Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file + */ + getDeploymentTarget?(projectData: IProjectData): any; + } +} diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts index a8bf78ad3b..e3982de1e4 100644 --- a/test/controllers/prepare-controller.ts +++ b/test/controllers/prepare-controller.ts @@ -38,7 +38,7 @@ function createTestInjector(data: { hasNativeChanges: boolean }): IInjector { }, }); - injector.register("webpackCompilerService", { + injector.register("bundlerCompilerService", { on: () => ({}), emit: () => ({}), compileWithWatch: async () => { @@ -119,7 +119,7 @@ describe("prepareController", () => { injector.resolve("prepareController"); const prepareNativePlatformService = injector.resolve( - "prepareNativePlatformService" + "prepareNativePlatformService", ); prepareNativePlatformService.prepareNativePlatform = async () => { const nativeFilesWatcher = (prepareController).watchersData[ @@ -128,7 +128,7 @@ describe("prepareController", () => { nativeFilesWatcher.emit( "all", "change", - "my/project/App_Resources/some/file" + "my/project/App_Resources/some/file", ); isNativePrepareCalled = true; return false; diff --git a/test/services/webpack/webpack-compiler-service.ts b/test/services/bundler/bundler-compiler-service.ts similarity index 62% rename from test/services/webpack/webpack-compiler-service.ts rename to test/services/bundler/bundler-compiler-service.ts index d15cccc430..49d69a55f4 100644 --- a/test/services/webpack/webpack-compiler-service.ts +++ b/test/services/bundler/bundler-compiler-service.ts @@ -1,5 +1,5 @@ import { Yok } from "../../../lib/common/yok"; -import { WebpackCompilerService } from "../../../lib/services/webpack/webpack-compiler-service"; +import { BundlerCompilerService } from "../../../lib/services/bundler/bundler-compiler-service"; import { assert } from "chai"; import { ErrorsStub } from "../../stubs"; import { IInjector } from "../../../lib/common/definitions/yok"; @@ -23,7 +23,7 @@ function createTestInjector(): IInjector { testInjector.register("packageManager", { getPackageManagerName: async () => "npm", }); - testInjector.register("webpackCompilerService", WebpackCompilerService); + testInjector.register("bundlerCompilerService", BundlerCompilerService); testInjector.register("childProcess", {}); testInjector.register("hooksService", {}); testInjector.register("hostInfo", {}); @@ -33,6 +33,9 @@ function createTestInjector(): IInjector { testInjector.register("packageInstallationManager", {}); testInjector.register("mobileHelper", {}); testInjector.register("cleanupService", {}); + testInjector.register("projectConfigService", { + getValue: (key: string, defaultValue?: string) => defaultValue, + }); testInjector.register("fs", { exists: (filePath: string) => true, }); @@ -40,23 +43,23 @@ function createTestInjector(): IInjector { return testInjector; } -describe("WebpackCompilerService", () => { +describe("BundlerCompilerService", () => { let testInjector: IInjector = null; - let webpackCompilerService: WebpackCompilerService = null; + let bundlerCompilerService: BundlerCompilerService = null; beforeEach(() => { testInjector = createTestInjector(); - webpackCompilerService = testInjector.resolve(WebpackCompilerService); + bundlerCompilerService = testInjector.resolve(BundlerCompilerService); }); describe("getUpdatedEmittedFiles", () => { // backwards compatibility with old versions of nativescript-dev-webpack it("should return only hot updates when nextHash is not provided", async () => { - const result = webpackCompilerService.getUpdatedEmittedFiles( + const result = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, null, - iOSPlatformName + iOSPlatformName, ); const expectedEmittedFiles = [ "bundle.hash1.hot-update.js", @@ -65,19 +68,19 @@ describe("WebpackCompilerService", () => { assert.deepStrictEqual(result.emittedFiles, expectedEmittedFiles); }); - // 2 successful webpack compilations + // 2 successful bundler compilations it("should return only hot updates when nextHash is provided", async () => { - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, "hash2", - iOSPlatformName + iOSPlatformName, ); - const result = webpackCompilerService.getUpdatedEmittedFiles( + const result = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash2"), chunkFiles, "hash3", - iOSPlatformName + iOSPlatformName, ); assert.deepStrictEqual(result.emittedFiles, [ @@ -85,19 +88,19 @@ describe("WebpackCompilerService", () => { "hash2.hot-update.json", ]); }); - // 1 successful webpack compilation, n compilations with no emitted files - it("should return all files when there is a webpack compilation with no emitted files", () => { - webpackCompilerService.getUpdatedEmittedFiles( + // 1 successful bundler compilation, n compilations with no emitted files + it("should return all files when there is a bundler compilation with no emitted files", () => { + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, "hash2", - iOSPlatformName + iOSPlatformName, ); - const result = webpackCompilerService.getUpdatedEmittedFiles( + const result = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash4"), chunkFiles, "hash5", - iOSPlatformName + iOSPlatformName, ); assert.deepStrictEqual(result.emittedFiles, [ @@ -107,25 +110,25 @@ describe("WebpackCompilerService", () => { "hash4.hot-update.json", ]); }); - // 1 successful webpack compilation, n compilations with no emitted files, 1 successful webpack compilation + // 1 successful bundler compilation, n compilations with no emitted files, 1 successful bundler compilation it("should return only hot updates after fixing the compilation error", () => { - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, "hash2", - iOSPlatformName + iOSPlatformName, ); - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash5"), chunkFiles, "hash6", - iOSPlatformName + iOSPlatformName, ); - const result = webpackCompilerService.getUpdatedEmittedFiles( + const result = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash6"), chunkFiles, "hash7", - iOSPlatformName + iOSPlatformName, ); assert.deepStrictEqual(result.emittedFiles, [ @@ -133,16 +136,16 @@ describe("WebpackCompilerService", () => { "hash6.hot-update.json", ]); }); - // 1 webpack compilation with no emitted files + // 1 bundler compilation with no emitted files it("should return all files when first compilation on livesync change is not successful", () => { - (webpackCompilerService).expectedHashes = { + (bundlerCompilerService).expectedHashes = { ios: "hash1", }; - const result = webpackCompilerService.getUpdatedEmittedFiles( + const result = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, "hash2", - iOSPlatformName + iOSPlatformName, ); assert.deepStrictEqual(result.emittedFiles, [ @@ -151,48 +154,48 @@ describe("WebpackCompilerService", () => { ]); }); it("should return correct hashes when there are more than one platform", () => { - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash1"), chunkFiles, "hash2", - iOSPlatformName + iOSPlatformName, ); - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash3"), chunkFiles, "hash4", - androidPlatformName + androidPlatformName, ); - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash2"), chunkFiles, "hash5", - iOSPlatformName + iOSPlatformName, ); - webpackCompilerService.getUpdatedEmittedFiles( + bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash4"), chunkFiles, "hash6", - androidPlatformName + androidPlatformName, ); - const iOSResult = webpackCompilerService.getUpdatedEmittedFiles( + const iOSResult = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash5"), chunkFiles, "hash7", - iOSPlatformName + iOSPlatformName, ); assert.deepStrictEqual(iOSResult.emittedFiles, [ "bundle.hash5.hot-update.js", "hash5.hot-update.json", ]); - const androidResult = webpackCompilerService.getUpdatedEmittedFiles( + const androidResult = bundlerCompilerService.getUpdatedEmittedFiles( getAllEmittedFiles("hash6"), chunkFiles, "hash8", - androidPlatformName + androidPlatformName, ); assert.deepStrictEqual(androidResult.emittedFiles, [ "bundle.hash6.hot-update.js", @@ -202,33 +205,33 @@ describe("WebpackCompilerService", () => { }); describe("compileWithWatch", () => { - it("fails when the value set for webpackConfigPath is not existant file", async () => { - const webpackConfigPath = "some path.js"; + it("fails when the value set for bundlerConfigPath is not existant file", async () => { + const bundlerConfigPath = "some path.js"; testInjector.resolve("fs").exists = (filePath: string) => - filePath !== webpackConfigPath; + filePath !== bundlerConfigPath; await assert.isRejected( - webpackCompilerService.compileWithWatch( + bundlerCompilerService.compileWithWatch( { platformNameLowerCase: "android" }, - { webpackConfigPath }, - {} + { bundlerConfigPath: bundlerConfigPath }, + {}, ), - `The webpack configuration file ${webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}` + `The bundler configuration file ${bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}`, ); }); }); describe("compileWithoutWatch", () => { - it("fails when the value set for webpackConfigPath is not existant file", async () => { - const webpackConfigPath = "some path.js"; + it("fails when the value set for bundlerConfigPath is not existant file", async () => { + const bundlerConfigPath = "some path.js"; testInjector.resolve("fs").exists = (filePath: string) => - filePath !== webpackConfigPath; + filePath !== bundlerConfigPath; await assert.isRejected( - webpackCompilerService.compileWithoutWatch( + bundlerCompilerService.compileWithoutWatch( { platformNameLowerCase: "android" }, - { webpackConfigPath }, - {} + { bundlerConfigPath: bundlerConfigPath }, + {}, ), - `The webpack configuration file ${webpackConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}` + `The bundler configuration file ${bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}`, ); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 404660d467..7ec3580891 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -38,6 +38,7 @@ import { IProjectConfigInformation, IProjectBackupService, IBackup, + BundlerType, } from "../lib/definitions/project"; import { IPlatformData, @@ -659,6 +660,7 @@ export class ProjectDataStub implements IProjectData { projectName: string; webpackConfigPath: string; bundlerConfigPath: string; + bundler: BundlerType; get platformsDir(): string { return ( From 4e88a351352f695e320e398a8852cb890fc12bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20de=20Dios=20Mart=C3=ADnez=20Vallejo?= Date: Tue, 25 Mar 2025 00:42:18 +0100 Subject: [PATCH 2/3] chore: cleanup --- .../bundler_Asdf/bundler-compiler-service.ts | 727 ------------------ lib/services/bundler_Asdf/bundler.d.ts | 226 ------ 2 files changed, 953 deletions(-) delete mode 100644 lib/services/bundler_Asdf/bundler-compiler-service.ts delete mode 100644 lib/services/bundler_Asdf/bundler.d.ts diff --git a/lib/services/bundler_Asdf/bundler-compiler-service.ts b/lib/services/bundler_Asdf/bundler-compiler-service.ts deleted file mode 100644 index 65b496a2b3..0000000000 --- a/lib/services/bundler_Asdf/bundler-compiler-service.ts +++ /dev/null @@ -1,727 +0,0 @@ -import * as path from "path"; -import * as child_process from "child_process"; -import * as semver from "semver"; -import * as _ from "lodash"; -import { EventEmitter } from "events"; -import { performanceLog } from "../../common/decorators"; -import { - WEBPACK_PLUGIN_NAME, - BUNDLER_COMPILATION_COMPLETE, - PackageManagers, - CONFIG_FILE_NAME_DISPLAY, -} from "../../constants"; -import { - IPackageManager, - IPackageInstallationManager, - IOptions, -} from "../../declarations"; -import { IPlatformData } from "../../definitions/platform"; -import { - BundlerType, - IProjectConfigService, - IProjectData, -} from "../../definitions/project"; -import { - IDictionary, - IErrors, - IStringDictionary, - IChildProcess, - IFileSystem, - IHooksService, - IHostInfo, -} from "../../common/declarations"; -import { ICleanupService } from "../../definitions/cleanup-service"; -import { injector } from "../../common/yok"; -import { - resolvePackagePath, - resolvePackageJSONPath, -} from "../../helpers/package-path-helper"; - -// todo: move out of here -interface IBundlerMessage { - type: "compilation" | "hmr-status"; - version?: number; - hash?: string; - data?: T; -} - -interface IBundlerCompilation { - emittedAssets: string[]; - staleAssets: string[]; -} - -export class BundlerCompilerService - extends EventEmitter - implements IBundlerCompilerService -{ - private bundlerProcesses: IDictionary = {}; - private expectedHashes: IStringDictionary = {}; - - constructor( - private $options: IOptions, - private $errors: IErrors, - private $childProcess: IChildProcess, - public $fs: IFileSystem, - public $hooksService: IHooksService, - public $hostInfo: IHostInfo, - private $logger: ILogger, - private $mobileHelper: Mobile.IMobileHelper, - private $cleanupService: ICleanupService, - private $packageManager: IPackageManager, - private $packageInstallationManager: IPackageInstallationManager, - private $projectConfigService: IProjectConfigService, - ) { - super(); - } - - public async compileWithWatch( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise { - return new Promise(async (resolve, reject) => { - if (this.bundlerProcesses[platformData.platformNameLowerCase]) { - resolve(void 0); - return; - } - - let isFirstBundlerWatchCompilation = true; - prepareData.watch = true; - try { - const childProcess = await this.startBundleProcess( - 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: string | IBundlerEmitMessage) => { - this.$logger.trace(`Message from ${projectData.bundler}`, 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 (isFirstBundlerWatchCompilation) { - isFirstBundlerWatchCompilation = 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 as IBundlerMessage, - platformData, - projectData, - prepareData, - ); - } - - if ( - message === - `${capitalizeFirstLetter(projectData.bundler)} compilation complete.` - ) { - this.$logger.info( - `${capitalizeFirstLetter(projectData.bundler)} build done!`, - ); - resolve(childProcess); - } - - message = message as IBundlerEmitMessage; - if (message.emittedFiles) { - if (isFirstBundlerWatchCompilation) { - isFirstBundlerWatchCompilation = 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: string) => - path.join( - platformData.appDestinationDirectoryPath, - this.$options.hostProjectModuleName, - file, - ), - ); - const fallbackFiles = result.fallbackFiles.map((file: string) => - 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 ${projectData.bundler} 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(BUNDLER_COMPILATION_COMPLETE, data); - } - } - }); - - childProcess.on("error", (err) => { - this.$logger.trace( - `Unable to start ${projectData.bundler} process in watch mode. Error is: ${err}`, - ); - delete this.bundlerProcesses[platformData.platformNameLowerCase]; - reject(err); - }); - - childProcess.on("close", async (arg: any) => { - await this.$cleanupService.removeKillProcess( - childProcess.pid.toString(), - ); - - const exitCode = typeof arg === "number" ? arg : arg && arg.code; - this.$logger.trace( - `${capitalizeFirstLetter(projectData.bundler)} process exited with code ${exitCode} when we expected it to be long living with watch.`, - ); - const error: any = new Error( - `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, - ); - error.code = exitCode; - delete this.bundlerProcesses[platformData.platformNameLowerCase]; - reject(error); - }); - } catch (err) { - reject(err); - } - }); - } - - public async compileWithoutWatch( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise { - return new Promise(async (resolve, reject) => { - if (this.bundlerProcesses[platformData.platformNameLowerCase]) { - resolve(); - return; - } - - try { - const childProcess = await this.startBundleProcess( - platformData, - projectData, - prepareData, - ); - - childProcess.on("error", (err) => { - this.$logger.trace( - `Unable to start ${projectData.bundler} process in non-watch mode. Error is: ${err}`, - ); - delete this.bundlerProcesses[platformData.platformNameLowerCase]; - reject(err); - }); - - childProcess.on("close", async (arg: any) => { - await this.$cleanupService.removeKillProcess( - childProcess.pid.toString(), - ); - - delete this.bundlerProcesses[platformData.platformNameLowerCase]; - const exitCode = typeof arg === "number" ? arg : arg && arg.code; - if (exitCode === 0) { - resolve(); - } else { - const error: any = new Error( - `Executing ${projectData.bundler} failed with exit code ${exitCode}.`, - ); - error.code = exitCode; - reject(error); - } - }); - } catch (err) { - reject(err); - } - }); - } - - public async stopBundlerCompiler(platform: string): Promise { - if (platform) { - await this.stopBundlerForPlatform(platform); - } else { - const bundlerPlatforms = Object.keys(this.bundlerProcesses); - - for (let i = 0; i < bundlerPlatforms.length; i++) { - await this.stopBundlerForPlatform(bundlerPlatforms[i]); - } - } - } - - private async shouldUsePreserveSymlinksOption(): Promise { - // 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 !== PackageManagers.pnpm; - return res; - } - - @performanceLog() - private async startBundleProcess( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise { - if (projectData.bundlerConfigPath) { - if (!this.$fs.exists(projectData.bundlerConfigPath)) { - this.$errors.fail( - `The bundler configuration file ${projectData.bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${CONFIG_FILE_NAME_DISPLAY}.`, - ); - } - } else { - if (!this.$fs.exists(projectData.bundlerConfigPath)) { - this.$errors.fail( - `The ${projectData.bundler} configuration file ${projectData.bundlerConfigPath} does not exist. Ensure the file exists, or update the path in ${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.getBundlerExecutablePath(projectData), - this.isModernBundler(projectData) ? `build` : null, - `--config=${projectData.bundlerConfigPath}`, - ...envParams, - ].filter(Boolean); - - if (prepareData.watch) { - args.push("--watch"); - } - - const stdio = prepareData.watch ? ["ipc"] : "inherit"; - const options: { [key: string]: any } = { - cwd: projectData.projectDir, - stdio, - }; - options.env = { - NATIVESCRIPT_WEBPACK_ENV: JSON.stringify(envData), - NATIVESCRIPT_BUNDLER_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.bundlerProcesses[platformData.platformNameLowerCase] = childProcess; - await this.$cleanupService.addKillProcess(childProcess.pid.toString()); - - return childProcess; - } - - private buildEnvData( - platform: string, - projectData: IProjectData, - prepareData: IPrepareData, - ) { - 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 = - process.env.NATIVESCRIPT_CONFIG_NAME ?? this.$options.config ?? "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 prepareData?.env?.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; - } - - private async buildEnvCommandLineParams( - envData: any, - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ) { - 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) { - if (projectData.bundler === "webpack") { - //TODO: check this use case for webpack5 WEBPACK_PLUGIN_NAME - const minWebpackPluginWithWinSnapshotsVersion = "1.3.0"; - const installedWebpackPluginVersion = - await this.$packageInstallationManager.getInstalledDependencyVersion( - 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 ${WEBPACK_PLUGIN_NAME}@latest).`, - ); - } - } - } - } - - const args: any[] = []; - 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: any) => args.push(`--env.${item}=${value}`)); - } - }); - - return args; - } - - public getUpdatedEmittedFiles( - allEmittedFiles: string[], - chunkFiles: string[], - nextHash: string, - platform: string, - ) { - 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, - }; - } - - private getCurrentHotUpdateHash(emittedFiles: string[]) { - 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 || ""; - } - - private async stopBundlerForPlatform(platform: string) { - this.$logger.trace( - `Stopping ${this.getBundler()} watch for platform ${platform}.`, - ); - const bundlerProcess = this.bundlerProcesses[platform]; - await this.$cleanupService.removeKillProcess(bundlerProcess.pid.toString()); - if (bundlerProcess) { - bundlerProcess.kill("SIGINT"); - delete this.bundlerProcesses[platform]; - } - } - - private handleHMRMessage( - message: IBundlerMessage, - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ) { - // handle new bundler hmr packets - this.$logger.trace( - `Received message from ${projectData.bundler} process:`, - message, - ); - - if (message.type !== "compilation") { - return; - } - - this.$logger.trace( - `${capitalizeFirstLetter(projectData.bundler)} build done!`, - ); - - const files = message.data.emittedAssets.map((asset: string) => - path.join( - platformData.appDestinationDirectoryPath, - this.$options.hostProjectModuleName, - asset, - ), - ); - const staleFiles = message.data.staleAssets.map((asset: string) => - path.join( - platformData.appDestinationDirectoryPath, - this.$options.hostProjectModuleName, - asset, - ), - ); - - // extract last hash from emitted filenames - const lastHash = (() => { - const absoluteFileNameWithLastHash = files.find((fileName: string) => - 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(BUNDLER_COMPILATION_COMPLETE, { - files, - staleFiles, - hasOnlyHotUpdateFiles: prepareData.hmr, - hmrData: { - hash: lastHash || message.hash, - fallbackFiles: [], - }, - platform: platformData.platformNameLowerCase, - }); - } - - private getBundlerExecutablePath(projectData: IProjectData): string { - const bundler = this.getBundler(); - - if (this.isModernBundler(projectData)) { - const packagePath = resolvePackagePath(`@nativescript/${bundler}`, { - paths: [projectData.projectDir], - }); - - if (packagePath) { - return path.resolve(packagePath, "dist", "bin", "index.js"); - } - } - - const packagePath = resolvePackagePath("webpack", { - paths: [projectData.projectDir], - }); - - if (!packagePath) { - return ""; - } - - return path.resolve(packagePath, "bin", "webpack.js"); - } - - private isModernBundler(projectData: IProjectData): boolean { - const bundler = this.getBundler(); - switch (bundler) { - case "rspack": - return true; - default: - const packageJSONPath = resolvePackageJSONPath(WEBPACK_PLUGIN_NAME, { - 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; - } - } - break; - } - - return false; - } - - public getBundler(): BundlerType { - return this.$projectConfigService.getValue(`bundler`, "webpack"); - } -} - -function capitalizeFirstLetter(val: string) { - return String(val).charAt(0).toUpperCase() + String(val).slice(1); -} - -injector.register("bundlerCompilerService", BundlerCompilerService); diff --git a/lib/services/bundler_Asdf/bundler.d.ts b/lib/services/bundler_Asdf/bundler.d.ts deleted file mode 100644 index 691d45f8c5..0000000000 --- a/lib/services/bundler_Asdf/bundler.d.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { EventEmitter } from "events"; -import { BuildData } from "../../data/build-data"; -import { PrepareData } from "../../data/prepare-data"; -import { - IPlatformProjectServiceBase, - IProjectData, - IValidatePlatformOutput, -} from "../../definitions/project"; -import { IOptions, IDependencyData } from "../../declarations"; -import { IPlatformData } from "../../definitions/platform"; -import { IPluginData } from "../../definitions/plugins"; -import { IRelease, ISpawnResult } from "../../common/declarations"; -import { - IProjectChangesInfo, - IPrepareInfo, - IAddedNativePlatform, -} from "../../definitions/project-changes"; -import { INotConfiguredEnvOptions } from "../../common/definitions/commands"; - -declare global { - interface IBundlerCompilerService extends EventEmitter { - compileWithWatch( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise; - compileWithoutWatch( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise; - stopBundlerCompiler(platform: string): Promise; - } - - interface IBundlerEnvOptions { - sourceMap?: boolean; - uglify?: boolean; - production?: boolean; - } - - interface IProjectChangesService { - checkForChanges( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise; - getPrepareInfoFilePath(platformData: IPlatformData): string; - getPrepareInfo(platformData: IPlatformData): IPrepareInfo; - savePrepareInfo( - platformData: IPlatformData, - projectData: IProjectData, - prepareData: IPrepareData, - ): Promise; - setNativePlatformStatus( - platformData: IPlatformData, - projectData: IProjectData, - addedPlatform: IAddedNativePlatform, - ): void; - currentChanges: IProjectChangesInfo; - } - - interface IFilesChangeEventData { - platform: string; - files: string[]; - staleFiles: string[]; - hmrData: IPlatformHmrData; - hasOnlyHotUpdateFiles: boolean; - hasNativeChanges: boolean; - } - - interface IBundlerEmitMessage { - emittedFiles: string[]; - chunkFiles: string[]; - hash: string; - } - - interface IPlatformProjectService - extends NodeJS.EventEmitter, - IPlatformProjectServiceBase { - getPlatformData(projectData: IProjectData): IPlatformData; - validate( - projectData: IProjectData, - options: IOptions, - notConfiguredEnvOptions?: INotConfiguredEnvOptions, - ): Promise; - createProject( - frameworkDir: string, - frameworkVersion: string, - projectData: IProjectData, - ): Promise; - interpolateData(projectData: IProjectData): Promise; - interpolateConfigurationFile(projectData: IProjectData): void; - - /** - * Executes additional actions after native project is created. - * @param {string} projectRoot Path to the real NativeScript project. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - afterCreateProject(projectRoot: string, projectData: IProjectData): void; - - /** - * Gets first chance to validate the options provided as command line arguments. - * @param {string} projectId Project identifier - for example org.nativescript.test. - * @param {any} provision UUID of the provisioning profile used in iOS option validation. - * @returns {void} - */ - validateOptions( - projectId?: string, - provision?: true | string, - teamId?: true | string, - ): Promise; - - buildProject( - projectRoot: string, - projectData: IProjectData, - buildConfig: T, - ): Promise; - - /** - * Prepares images in Native project (for iOS). - * @param {IProjectData} projectData DTO with information about the project. - * @param {any} platformSpecificData Platform specific data required for project preparation. - * @returns {void} - */ - prepareProject( - projectData: IProjectData, - prepareData: T, - ): Promise; - - /** - * Prepares App_Resources in the native project by clearing data from other platform and applying platform specific rules. - * @param {string} appResourcesDirectoryPath The place in the native project where the App_Resources are copied first. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - prepareAppResources(projectData: IProjectData): void; - - /** - * Defines if current platform is prepared (i.e. if /platforms/ dir exists). - * @param {string} projectRoot The project directory (path where root's package.json is located). - * @param {IProjectData} projectData DTO with information about the project. - * @returns {boolean} True in case platform is prepare (i.e. if /platforms/ dir exists), false otherwise. - */ - isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean; - - preparePluginNativeCode( - pluginData: IPluginData, - options?: any, - ): Promise; - - /** - * Removes native code of a plugin (CocoaPods, jars, libs, src). - * @param {IPluginData} Plugins data describing the plugin which should be cleaned. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - removePluginNativeCode( - pluginData: IPluginData, - projectData: IProjectData, - ): Promise; - - beforePrepareAllPlugins( - projectData: IProjectData, - dependencies?: IDependencyData[], - ): Promise; - - handleNativeDependenciesChange( - projectData: IProjectData, - opts: IRelease, - ): Promise; - - /** - * Gets the path wheren App_Resources should be copied. - * @returns {string} Path to native project, where App_Resources should be copied. - */ - getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string; - - cleanDeviceTempFolder( - deviceIdentifier: string, - projectData: IProjectData, - ): Promise; - processConfigurationFilesFromAppResources( - projectData: IProjectData, - opts: { release: boolean }, - ): Promise; - - /** - * Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {void} - */ - ensureConfigurationFileInAppResources(projectData: IProjectData): void; - - /** - * Stops all running processes that might hold a lock on the filesystem. - * Android: Gradle daemon processes are terminated. - * @param {IPlatformData} platformData The data for the specified platform. - * @returns {void} - */ - stopServices?(projectRoot: string): Promise; - - /** - * Removes build artifacts specific to the platform - * @param {string} projectRoot The root directory of the native project. - * @returns {void} - */ - cleanProject?(projectRoot: string): Promise; - - /** - * Check the current state of the project, and validate against the options. - * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. - */ - checkForChanges( - changeset: IProjectChangesInfo, - prepareData: T, - projectData: IProjectData, - ): Promise; - - /** - * Get the deployment target's version - * Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file - */ - getDeploymentTarget?(projectData: IProjectData): any; - } -} From 39377a9a1486963ee074f0d0968d658c27adf18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20de=20Dios=20Mart=C3=ADnez=20Vallejo?= Date: Tue, 25 Mar 2025 17:32:30 +0100 Subject: [PATCH 3/3] fix: bundlerConfigPath should use webpackConfigPath and add test for bundler and bundlerConfigPath --- lib/project-data.ts | 14 +++---- test/project-data.ts | 93 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/lib/project-data.ts b/lib/project-data.ts index cbe06da65f..277dbf32d1 100644 --- a/lib/project-data.ts +++ b/lib/project-data.ts @@ -207,21 +207,17 @@ export class ProjectData implements IProjectData { constants.PODFILE_NAME, ); this.isShared = !!(this.nsConfig && this.nsConfig.shared); - this.webpackConfigPath = + + const webpackConfigPath = this.nsConfig && this.nsConfig.webpackConfigPath ? path.resolve(this.projectDir, this.nsConfig.webpackConfigPath) : path.join(this.projectDir, "webpack.config.js"); + this.webpackConfigPath = webpackConfigPath; this.bundlerConfigPath = this.nsConfig && this.nsConfig.bundlerConfigPath ? path.resolve(this.projectDir, this.nsConfig.bundlerConfigPath) - : null; - this.bundler = - this.nsConfig && this.nsConfig.bundler - ? (path.resolve( - this.projectDir, - this.nsConfig.bundler, - ) as BundlerType) - : "webpack"; + : webpackConfigPath; + this.bundler = this?.nsConfig?.bundler ?? "webpack"; return; } diff --git a/test/project-data.ts b/test/project-data.ts index 7ce33a1320..5b8c747bd1 100644 --- a/test/project-data.ts +++ b/test/project-data.ts @@ -56,14 +56,16 @@ describe("projectData", () => { configData?: { shared?: boolean; webpackConfigPath?: string; + bundlerConfigPath?: string; projectName?: string; + bundler?: string; }; }): IProjectData => { const testInjector = createTestInjector(); const fs = testInjector.resolve("fs"); testInjector.register( "projectConfigService", - stubs.ProjectConfigServiceStub.initWithConfig(opts?.configData) + stubs.ProjectConfigServiceStub.initWithConfig(opts?.configData), ); fs.exists = (filePath: string) => { @@ -98,7 +100,7 @@ describe("projectData", () => { const assertProjectType = ( dependencies: any, devDependencies: any, - expectedProjecType: string + expectedProjecType: string, ) => { const projectData = prepareTest({ packageJsonData: { @@ -125,7 +127,7 @@ describe("projectData", () => { assertProjectType( { "nativescript-vue": "*" }, { typescript: "*" }, - "Vue.js" + "Vue.js", ); }); @@ -141,7 +143,7 @@ describe("projectData", () => { assertProjectType( null, { "nativescript-dev-typescript": "*" }, - "Pure TypeScript" + "Pure TypeScript", ); }); @@ -195,13 +197,13 @@ describe("projectData", () => { const projectData = prepareTest(); assert.equal( projectData.webpackConfigPath, - path.join(projectDir, "webpack.config.js") + path.join(projectDir, "webpack.config.js"), ); }); it("returns correct path when full path is set in nsconfig.json", () => { const pathToConfig = path.resolve( - path.join("/testDir", "innerDir", "mywebpack.config.js") + path.join("/testDir", "innerDir", "mywebpack.config.js"), ); const projectData = prepareTest({ configData: { webpackConfigPath: pathToConfig }, @@ -211,7 +213,7 @@ describe("projectData", () => { it("returns correct path when relative path is set in nsconfig.json", () => { const pathToConfig = path.resolve( - path.join("projectDir", "innerDir", "mywebpack.config.js") + path.join("projectDir", "innerDir", "mywebpack.config.js"), ); const projectData = prepareTest({ configData: { @@ -221,4 +223,81 @@ describe("projectData", () => { assert.equal(projectData.webpackConfigPath, pathToConfig); }); }); + + describe("bundlerConfigPath", () => { + it("default path to webpack.config.js is set when nsconfig.json does not set value", () => { + const projectData = prepareTest(); + assert.equal( + projectData.bundlerConfigPath, + path.join(projectDir, "webpack.config.js"), + ); + }); + + it("should use webpackConfigPath property when bundlerConfigPath is not defined", () => { + const pathToConfig = path.resolve( + path.join("/testDir", "innerDir", "mywebpack.config.js"), + ); + const projectData = prepareTest({ + configData: { webpackConfigPath: pathToConfig }, + }); + assert.equal(projectData.bundlerConfigPath, pathToConfig); + }); + + it("returns correct path when full path is set in nsconfig.json", () => { + const pathToConfig = path.resolve( + path.join("/testDir", "innerDir", "mywebpack.config.js"), + ); + const projectData = prepareTest({ + configData: { bundlerConfigPath: pathToConfig }, + }); + assert.equal(projectData.bundlerConfigPath, pathToConfig); + }); + + it("returns correct path when relative path is set in nsconfig.json", () => { + const pathToConfig = path.resolve( + path.join("projectDir", "innerDir", "mywebpack.config.js"), + ); + const projectData = prepareTest({ + configData: { + bundlerConfigPath: path.join("./innerDir", "mywebpack.config.js"), + }, + }); + assert.equal(projectData.bundlerConfigPath, pathToConfig); + }); + + it("should use bundlerConfigPath instead of webpackConfigPath if both are defined.", () => { + const pathToConfig = path.resolve( + path.join("projectDir", "innerDir", "myrspack.config.js"), + ); + const projectData = prepareTest({ + configData: { + webpackConfigPath: path.join("./innerDir", "mywebpack.config.js"), + bundlerConfigPath: path.join("./innerDir", "myrspack.config.js"), + }, + }); + assert.equal(projectData.bundlerConfigPath, pathToConfig); + }); + }); + + describe("bundler", () => { + it("sets bundler to 'webpack' by default when nsconfig.json does not specify a bundler", () => { + const projectData = prepareTest(); + assert.equal(projectData.bundler, "webpack"); + }); + + it("sets bundler to 'webpack' when explicitly defined in nsconfig.json", () => { + const projectData = prepareTest({ configData: { bundler: "webpack" } }); + assert.equal(projectData.bundler, "webpack"); + }); + + it("sets bundler to 'rspack' when explicitly defined in nsconfig.json", () => { + const projectData = prepareTest({ configData: { bundler: "rspack" } }); + assert.equal(projectData.bundler, "rspack"); + }); + + it("sets bundler to 'vite' when explicitly defined in nsconfig.json", () => { + const projectData = prepareTest({ configData: { bundler: "vite" } }); + assert.equal(projectData.bundler, "vite"); + }); + }); });