diff --git a/docs/man_pages/project/hooks/hooks.md b/docs/man_pages/project/hooks/hooks.md new file mode 100644 index 0000000000..0c952aedc7 --- /dev/null +++ b/docs/man_pages/project/hooks/hooks.md @@ -0,0 +1,35 @@ +<% if (isJekyll) { %>--- +title: ns hooks +position: 1 +---<% } %> + +# ns create + +### Description + +Manages lifecycle hooks from installed plugins. + +### Commands + +Usage | Synopsis +---------|--------- +Install | `$ ns hooks install` +List | `$ ns hooks list` +Lock | `$ ns hooks lock` +Verify | `$ ns hooks verify` + +#### Install + +Installs hooks from each installed plugin dependency. + +#### List + +Lists the plugins which have hooks and which scripts they install + +#### Lock + +Generates a `hooks-lock.json` containing the hooks that are in the current versions of the plugins. + +#### Verify + +Verifies that the hooks contained in the installed plugins match those listed in the `hooks-lock.json` file. diff --git a/docs/man_pages/start.md b/docs/man_pages/start.md index 83b9da83e8..646952bb68 100644 --- a/docs/man_pages/start.md +++ b/docs/man_pages/start.md @@ -51,6 +51,7 @@ Command | Description [plugin](lib-management/plugin.html) | Lets you manage the plugins for your project. [open](project/configuration/open.md) | Opens the native project in Xcode/Android Studio. [widget ios](project/configuration/widget.md) | Adds a new iOS widget to the project. +[hooks](project/hooks/hooks.html) | Installs lifecycle hooks from plugins. ## Publishing Commands Command | Description ---|--- diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index ad181a1062..3281d84930 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -263,6 +263,15 @@ injector.requireCommand("plugin|update", "./commands/plugin/update-plugin"); injector.requireCommand("plugin|build", "./commands/plugin/build-plugin"); injector.requireCommand("plugin|create", "./commands/plugin/create-plugin"); +injector.requireCommand( + ["hooks|*list", "hooks|install"], + "./commands/hooks/hooks", +); +injector.requireCommand( + ["hooks|lock", "hooks|verify"], + "./commands/hooks/hooks-lock", +); + injector.require("doctorService", "./services/doctor-service"); injector.require("xcprojService", "./services/xcproj-service"); injector.require("versionsService", "./services/versions-service"); diff --git a/lib/commands/hooks/common.ts b/lib/commands/hooks/common.ts new file mode 100644 index 0000000000..4532a73326 --- /dev/null +++ b/lib/commands/hooks/common.ts @@ -0,0 +1,118 @@ +import * as _ from "lodash"; +import { IProjectData } from "../../definitions/project"; +import { IPluginData } from "../../definitions/plugins"; +import { ICommandParameter } from "../../common/definitions/commands"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import path = require("path"); +import * as crypto from "crypto"; + +export const LOCK_FILE_NAME = "nativescript-lock.json"; +export interface OutputHook { + type: string; + hash: string; +} + +export interface OutputPlugin { + name: string; + hooks: OutputHook[]; +} + +export class HooksVerify { + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $projectData: IProjectData, + protected $errors: IErrors, + protected $fs: IFileSystem, + protected $logger: ILogger, + ) { + this.$projectData.initializeProjectData(); + } + + protected async verifyHooksLock( + plugins: IPluginData[], + hooksLockPath: string, + ): Promise { + let lockFileContent: string; + let hooksLock: OutputPlugin[]; + + try { + lockFileContent = this.$fs.readText(hooksLockPath, "utf8"); + hooksLock = JSON.parse(lockFileContent); + } catch (err) { + this.$errors.fail( + `❌ Failed to read or parse ${LOCK_FILE_NAME} at ${hooksLockPath}`, + ); + } + + const lockMap = new Map>(); // pluginName -> hookType -> hash + + for (const plugin of hooksLock) { + const hookMap = new Map(); + for (const hook of plugin.hooks) { + hookMap.set(hook.type, hook.hash); + } + lockMap.set(plugin.name, hookMap); + } + + let isValid = true; + + for (const plugin of plugins) { + const pluginLockHooks = lockMap.get(plugin.name); + + if (!pluginLockHooks) { + this.$logger.error( + `❌ Plugin '${plugin.name}' not found in ${LOCK_FILE_NAME}`, + ); + isValid = false; + continue; + } + + for (const hook of plugin.nativescript?.hooks || []) { + const expectedHash = pluginLockHooks.get(hook.type); + + if (!expectedHash) { + this.$logger.error( + `❌ Missing hook '${hook.type}' for plugin '${plugin.name}' in ${LOCK_FILE_NAME}`, + ); + isValid = false; + continue; + } + + let fileContent: string | Buffer; + + try { + fileContent = this.$fs.readFile( + path.join(plugin.fullPath, hook.script), + ); + } catch (err) { + this.$logger.error( + `❌ Cannot read script file '${hook.script}' for hook '${hook.type}' in plugin '${plugin.name}'`, + ); + isValid = false; + continue; + } + + const actualHash = crypto + .createHash("sha256") + .update(fileContent) + .digest("hex"); + + if (actualHash !== expectedHash) { + this.$logger.error( + `❌ Hash mismatch for '${hook.script}' (${hook.type} in ${plugin.name}):`, + ); + this.$logger.error(` Expected: ${expectedHash}`); + this.$logger.error(` Actual: ${actualHash}`); + isValid = false; + } + } + } + + if (isValid) { + this.$logger.info("✅ All hooks verified successfully. No issues found."); + } else { + this.$errors.fail("❌ One or more hooks failed verification."); + } + } +} diff --git a/lib/commands/hooks/hooks-lock.ts b/lib/commands/hooks/hooks-lock.ts new file mode 100644 index 0000000000..27399ac622 --- /dev/null +++ b/lib/commands/hooks/hooks-lock.ts @@ -0,0 +1,135 @@ +import { IProjectData } from "../../definitions/project"; +import { IPluginsService, IPluginData } from "../../definitions/plugins"; +import { ICommand, ICommandParameter } from "../../common/definitions/commands"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import { injector } from "../../common/yok"; +import path = require("path"); +import * as crypto from "crypto"; +import { + HooksVerify, + LOCK_FILE_NAME, + OutputHook, + OutputPlugin, +} from "./common"; + +export class HooksLockPluginCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + private $projectData: IProjectData, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(): Promise { + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + + await this.writeHooksLockFile( + pluginsWithHooks, + this.$projectData.projectDir, + ); + } else { + this.$logger.info("No plugins with hooks found."); + } + } + + public async canExecute(args: string[]): Promise { + return true; + } + + private async writeHooksLockFile( + plugins: IPluginData[], + outputDir: string, + ): Promise { + const output: OutputPlugin[] = []; + + for (const plugin of plugins) { + const hooks: OutputHook[] = []; + + for (const hook of plugin.nativescript?.hooks || []) { + try { + const fileContent = this.$fs.readFile( + path.join(plugin.fullPath, hook.script), + ); + const hash = crypto + .createHash("sha256") + .update(fileContent) + .digest("hex"); + + hooks.push({ + type: hook.type, + hash, + }); + } catch (err) { + this.$logger.warn( + `Warning: Failed to read script '${hook.script}' for plugin '${plugin.name}'. Skipping this hook.`, + ); + continue; + } + } + + output.push({ name: plugin.name, hooks }); + } + + const filePath = path.resolve(outputDir, LOCK_FILE_NAME); + + try { + this.$fs.writeFile(filePath, JSON.stringify(output, null, 2), "utf8"); + this.$logger.info(`✅ ${LOCK_FILE_NAME} written to: ${filePath}`); + } catch (err) { + this.$errors.fail(`❌ Failed to write ${LOCK_FILE_NAME}: ${err}`); + } + } +} + +export class HooksVerifyPluginCommand extends HooksVerify implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, + ) { + super($projectData, $errors, $fs, $logger); + } + + public async execute(): Promise { + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + await this.verifyHooksLock( + pluginsWithHooks, + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ); + } else { + this.$logger.info("No plugins with hooks found."); + } + } + + public async canExecute(args: string[]): Promise { + return true; + } +} + +injector.registerCommand(["hooks|lock"], HooksLockPluginCommand); +injector.registerCommand(["hooks|verify"], HooksVerifyPluginCommand); diff --git a/lib/commands/hooks/hooks.ts b/lib/commands/hooks/hooks.ts new file mode 100644 index 0000000000..4971c648cf --- /dev/null +++ b/lib/commands/hooks/hooks.ts @@ -0,0 +1,104 @@ +import { IProjectData } from "../../definitions/project"; +import { IPluginsService, IPluginData } from "../../definitions/plugins"; +import { ICommand, ICommandParameter } from "../../common/definitions/commands"; +import { injector } from "../../common/yok"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import path = require("path"); +import { HOOKS_DIR_NAME } from "../../constants"; +import { createTable } from "../../common/helpers"; +import nsHooks = require("@nativescript/hook"); +import { HooksVerify, LOCK_FILE_NAME } from "./common"; + +export class HooksPluginCommand extends HooksVerify implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, + ) { + super($projectData, $errors, $fs, $logger); + } + + public async execute(args: string[]): Promise { + const isList: boolean = + args.length > 0 && args[0] === "list" ? true : false; + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const hooksDir = path.join(this.$projectData.projectDir, HOOKS_DIR_NAME); + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + + if (isList) { + const headers: string[] = ["Plugin", "HookName", "HookPath"]; + const hookDataData: string[][] = pluginsWithHooks.flatMap((plugin) => + plugin.nativescript.hooks.map( + (hook: { type: string; script: string }) => { + return [plugin.name, hook.type, hook.script]; + }, + ), + ); + const hookDataTable: any = createTable(headers, hookDataData); + this.$logger.info("Hooks:"); + this.$logger.info(hookDataTable.toString()); + } else { + if ( + this.$fs.exists( + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ) + ) { + await this.verifyHooksLock( + pluginsWithHooks, + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ); + } + + if (pluginsWithHooks.length === 0) { + if (!this.$fs.exists(hooksDir)) { + this.$fs.createDirectory(hooksDir); + } + } + for (const plugin of pluginsWithHooks) { + nsHooks(plugin.fullPath).postinstall(); + } + } + } + } + + public async canExecute(args: string[]): Promise { + if (args.length > 0 && args[0] !== "list") { + this.$errors.failWithHelp( + `Invalid argument ${args[0]}. Supported argument is "list".`, + ); + } + return true; + } +} + +export class HooksListPluginCommand extends HooksPluginCommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + $pluginsService: IPluginsService, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, + ) { + super($pluginsService, $projectData, $errors, $fs, $logger); + } + + public async execute(): Promise { + await super.execute(["list"]); + } +} + +injector.registerCommand(["hooks|install"], HooksPluginCommand); +injector.registerCommand(["hooks|*list"], HooksListPluginCommand); diff --git a/lib/definitions/hooks.d.ts b/lib/definitions/hooks.d.ts new file mode 100644 index 0000000000..c2c3932fc7 --- /dev/null +++ b/lib/definitions/hooks.d.ts @@ -0,0 +1 @@ +declare module "@nativescript/hook"; diff --git a/lib/nativescript-cli.ts b/lib/nativescript-cli.ts index f9932fc1a3..04b2f90fd3 100644 --- a/lib/nativescript-cli.ts +++ b/lib/nativescript-cli.ts @@ -3,6 +3,35 @@ require("./bootstrap"); import * as shelljs from "shelljs"; shelljs.config.silent = true; shelljs.config.fatal = true; + +if (process.platform === "win32") { + // Later versions of shelljs do not process globs with \ path delimiters correctly, for windows change to / + const realcp = shelljs.cp; + (shelljs as any).cp = (...args: unknown[]) => { + if (args.length === 3) { + args[1] = replaceDashes(args[1] as string | string[]); + } else { + args[0] = replaceDashes(args[0] as string | string[]); + } + + if (args.length == 2) { + realcp(args[0] as string[], args[1] as string); + } else { + realcp(args[0] as string, args[1] as string[], args[2] as string); + } + }; + function replaceDashes(values: string | string[]): string | string[] { + if (Array.isArray(values)) { + for (let i = 0; i < values.length; ++i) { + values[i] = replaceDashes(values[i]) as string; + } + return values; + } else { + return values.replace(/\\/g, "/"); + } + } +} + import { installUncaughtExceptionListener } from "./common/errors"; import { settlePromises } from "./common/helpers"; import { injector } from "./common/yok"; @@ -14,7 +43,7 @@ import { import { IInitializeService } from "./definitions/initialize-service"; import { color } from "./color"; installUncaughtExceptionListener( - process.exit.bind(process, ErrorCodes.UNCAUGHT) + process.exit.bind(process, ErrorCodes.UNCAUGHT), ); const logger: ILogger = injector.resolve("logger"); @@ -23,7 +52,7 @@ export const originalProcessOn = process.on.bind(process); process.on = (event: string, listener: any): any => { if (event === "SIGINT") { logger.trace( - `Trying to handle SIGINT event. CLI overrides this behavior and does not allow handling SIGINT as this causes issues with Ctrl + C in terminal.` + `Trying to handle SIGINT event. CLI overrides this behavior and does not allow handling SIGINT as this causes issues with Ctrl + C in terminal.`, ); const msg = "The stackTrace of the location trying to handle SIGINT is"; const stackTrace = new Error(msg).stack || ""; @@ -31,9 +60,9 @@ process.on = (event: string, listener: any): any => { stackTrace.replace( `Error: ${msg}`, `${msg} (${color.yellow( - "note:" - )} this is not an error, just a stack-trace for debugging purposes):` - ) + "note:", + )} this is not an error, just a stack-trace for debugging purposes):`, + ), ); } else { return originalProcessOn(event, listener); @@ -52,13 +81,12 @@ process.on = (event: string, listener: any): any => { const err: IErrors = injector.resolve("$errors"); err.printCallStack = config.DEBUG; - const $initializeService = injector.resolve( - "initializeService" - ); + const $initializeService = + injector.resolve("initializeService"); await $initializeService.initialize(); const extensibilityService: IExtensibilityService = injector.resolve( - "extensibilityService" + "extensibilityService", ); try { await settlePromises(extensibilityService.loadExtensions()); @@ -66,9 +94,8 @@ process.on = (event: string, listener: any): any => { logger.trace("Unable to load extensions. Error is: ", err); } - const commandDispatcher: ICommandDispatcher = injector.resolve( - "commandDispatcher" - ); + const commandDispatcher: ICommandDispatcher = + injector.resolve("commandDispatcher"); // unused... // const messages: IMessagesService = injector.resolve("$messagesService"); diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index a888600bcf..e090e94079 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -126,78 +126,37 @@ export class BundlerCompilerService } // Copy Vite output files directly to platform destination - const distOutput = path.join(projectData.projectDir, "dist"); + const distOutput = path.join( + projectData.projectDir, + ".ns-vite-build", + ); const destDir = path.join( platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, ); if (debugLog) { - console.log(`🔥 Copying from ${distOutput} to ${destDir}`); + console.log(`Copying from ${distOutput} to ${destDir}.`); } // Determine which files to copy based on build type and changes - if (message.isHMR) { - // HMR updates: only copy changed files - if (debugLog) { - console.log( - "🔥 HMR update - copying only changed files for:", - message.changedFiles, - ); - } - - // For HTML template changes, we need to copy the component files that were rebuilt - let filesToCopy = message.emittedFiles; - - // If we have HTML changes, identify which component files need copying - const hasHTMLChanges = message.changedFiles.some((f) => - f.endsWith(".html"), - ); - if (hasHTMLChanges) { - // Copy component-related files (the ones that would have been rebuilt due to template changes) - filesToCopy = message.emittedFiles.filter( - (f) => - f.includes(".component") || - f === "bundle.mjs" || - f === "bundle.mjs.map", - ); - if (debugLog) { - console.log( - "🔥 HTML change detected - copying component files:", - filesToCopy, - ); - } - } - - this.copyViteBundleToNative(distOutput, destDir, filesToCopy); - } else if ( + if ( message.buildType === "incremental" && - message.changedFiles && - message.changedFiles.length > 0 + message.emittedFiles && + message.emittedFiles.length > 0 ) { // Incremental builds: only copy files that are likely affected by the changes - if (debugLog) { - console.log( - "🔥 Incremental build - copying only relevant files for:", - message.changedFiles, - ); - } - const filesToCopy = this.getIncrementalFilesToCopy( message.emittedFiles, - message.changedFiles, ); if (debugLog) { - console.log( - "🔥 Incremental build - files to copy:", - filesToCopy, - ); + console.log("Incremental build - files to copy:", filesToCopy); } this.copyViteBundleToNative(distOutput, destDir, filesToCopy); } else { if (debugLog) { - console.log("🔥 Full build - copying all files"); + console.log("Full build - copying all files."); } this.copyViteBundleToNative(distOutput, destDir); } @@ -236,31 +195,14 @@ export class BundlerCompilerService this.$logger.info( `Vite build completed! Files copied to native platform.`, ); - // Send HMR notification to connected WebSocket clients first - this.notifyHMRClients({ - type: message.isHMR ? "js-update" : "build-complete", - timestamp: Date.now(), - changedFiles: message.changedFiles || [], - buildType: message.buildType || "incremental", - isHMR: message.isHMR || false, - }); - - if (message.isHMR) { - if (debugLog) { - console.log( - "🔥 Skipping BUNDLER_COMPILATION_COMPLETE for HMR update - app will not restart", - ); - } - } else { - // Only emit BUNDLER_COMPILATION_COMPLETE for non-HMR builds - // This prevents the CLI from restarting the app during HMR updates - if (debugLog) { - console.log( - "🔥 Emitting BUNDLER_COMPILATION_COMPLETE for full build", - ); - } - this.emit(BUNDLER_COMPILATION_COMPLETE, data); + + if (debugLog) { + console.log( + "Emitting BUNDLER_COMPILATION_COMPLETE for full build.", + ); } + this.emit(BUNDLER_COMPILATION_COMPLETE, data); + return; } @@ -510,7 +452,7 @@ export class BundlerCompilerService // Note: With Vite, we need `--` to prevent vite cli from erroring on unknown options. const envParams = isVite ? [ - `--mode=${platformData.platformNameLowerCase}`, + `--mode=${prepareData.release ? "production" : "development"}`, `--watch`, "--", ...cliArgs, @@ -530,7 +472,7 @@ export class BundlerCompilerService const args = [ ...additionalNodeArgs, this.getBundlerExecutablePath(projectData), - isVite ? "build" : this.isModernBundler(projectData) ? `build` : null, + isVite || this.isModernBundler(projectData) ? "build" : null, `--config=${projectData.bundlerConfigPath}`, ...envParams, ].filter(Boolean); @@ -910,55 +852,53 @@ export class BundlerCompilerService ) { // Clean and copy Vite output to native platform folder if (debugLog) { - console.log(`Copying Vite bundle from "${distOutput}" to "${destDir}"`); + console.log(`Copying Vite bundle from "${distOutput}" to "${destDir}".`); } - const fs = require("fs"); - try { if (specificFiles) { - // Selective mode: only copy specific files (HMR or incremental) + // Selective mode: only copy specific files (incremental) if (debugLog) { console.log( - "🔥 Selective copy - copying specific files:", + "Selective copy - copying specific files:", specificFiles, ); } // Ensure destination directory exists - fs.mkdirSync(destDir, { recursive: true }); + this.$fs.createDirectory(destDir); // Copy only the specified files for (const file of specificFiles) { const srcPath = path.join(distOutput, file); const destPath = path.join(destDir, file); - if (!fs.existsSync(srcPath)) continue; + if (!this.$fs.exists(srcPath)) continue; // create parent dirs - fs.mkdirSync(path.dirname(destPath), { recursive: true }); + this.$fs.createDirectory(path.dirname(destPath)); - fs.copyFileSync(srcPath, destPath); + this.$fs.copyFile(srcPath, destPath); if (debugLog) { - console.log(`🔥 Copied ${file}`); + console.log(`Copied ${file}`); } } } else { // Full build mode: clean and copy everything if (debugLog) { - console.log("🔥 Full build: Copying all files"); + console.log("Full build: Copying all files."); } // Clean destination directory - if (fs.existsSync(destDir)) { - fs.rmSync(destDir, { recursive: true, force: true }); + if (this.$fs.exists(destDir)) { + this.$fs.deleteDirectory(destDir); } - fs.mkdirSync(destDir, { recursive: true }); + this.$fs.createDirectory(destDir); // Copy all files from dist to platform destination - if (fs.existsSync(distOutput)) { - this.copyRecursiveSync(distOutput, destDir, fs); + if (this.$fs.exists(distOutput)) { + this.copyRecursiveSync(distOutput, destDir); } else { this.$logger.warn( `Vite output directory does not exist: ${distOutput}`, @@ -970,51 +910,31 @@ export class BundlerCompilerService } } - private getIncrementalFilesToCopy( - emittedFiles: string[], - changedFiles: string[], - ): string[] { + private getIncrementalFilesToCopy(emittedFiles: string[]): string[] { // For incremental builds, we need to determine which emitted files are likely affected // by the source file changes const filesToCopy: string[] = []; - // Always copy bundle files as they contain the compiled source code - // ignoring vendor files as they are less likely to change frequently + // default to ignoring vendor files as they are less likely to change during live reloads const bundleFiles = emittedFiles.filter( (file) => !file.includes("vendor") && - (file.includes("bundle") || - file.includes("main") || - file.includes("app") || - file.endsWith(".mjs") || - file.endsWith(".js")), + (file.endsWith(".mjs") || + file.endsWith(".js") || + file.endsWith(".map")), ); filesToCopy.push(...bundleFiles); - // Always copy source maps for debugging - const sourceMapFiles = emittedFiles.filter( - (file) => !file.includes("vendor") && file.endsWith(".map"), - ); - filesToCopy.push(...sourceMapFiles); - - // Only handle assets if they're explicitly referenced in the changed files - const hasAssetChanges = changedFiles.some( + // Only copy assets if there are explicit asset-related changes + const assetFiles = emittedFiles.filter( (file) => - file.includes("/assets/") || - file.includes("/static/") || - file.includes("/public/"), + file.includes("assets/") || + file.includes("static/") || + file.includes("fonts/") || + file.includes("images/"), ); - - if (hasAssetChanges) { - // Only copy assets if there are explicit asset-related changes - const assetFiles = emittedFiles.filter( - (file) => - file.includes("assets/") || - file.includes("static/") || - file.includes("fonts/") || - file.includes("images/"), - ); + if (assetFiles.length > 0) { filesToCopy.push(...assetFiles); } @@ -1022,48 +942,22 @@ export class BundlerCompilerService return [...new Set(filesToCopy)]; } - private notifyHMRClients(message: any) { - // Send WebSocket notification to HMR clients - try { - const WebSocket = require("ws"); - - // Try to connect to HMR bridge and send notification - const ws = new WebSocket("ws://localhost:24678"); - - ws.on("open", () => { - if (debugLog) { - console.log("🔥 Sending HMR notification to bridge:", message.type); - } - ws.send(JSON.stringify(message)); - ws.close(); - }); - - ws.on("error", () => { - // HMR bridge not available, which is fine - if (debugLog) { - console.log( - "🔥 HMR bridge not available (this is normal without HMR)", - ); - } - }); - } catch (error) { - // WebSocket not available, which is fine - if (debugLog) { - console.log("🔥 WebSocket not available for HMR notifications"); - } - } - } - - private copyRecursiveSync(src: string, dest: string, fs: any) { - for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - - if (entry.isDirectory()) { - fs.mkdirSync(destPath, { recursive: true }); - this.copyRecursiveSync(srcPath, destPath, fs); - } else if (entry.isFile() || entry.isSymbolicLink()) { - fs.copyFileSync(srcPath, destPath); + private copyRecursiveSync(src: string, dest: string) { + // Ensure destination exists + this.$fs.createDirectory(dest); + + const entries = this.$fs.readDirectory(src); + for (const name of entries) { + const srcPath = path.join(src, name); + const destPath = path.join(dest, name); + const lstats = this.$fs.getLsStats(srcPath); + + if (lstats.isDirectory()) { + this.copyRecursiveSync(srcPath, destPath); + } else if (lstats.isFile() || lstats.isSymbolicLink()) { + // create parent directory (copyFile will also ensure it, but keep explicit) + this.$fs.createDirectory(path.dirname(destPath)); + this.$fs.copyFile(srcPath, destPath); } } } diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index e9b5e6a54c..2fadc8fe16 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -624,7 +624,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple projectDir: string ): IPluginData { try { - const pluginData: any = {}; + const pluginData: IPluginData = {}; pluginData.name = cacheData.name; pluginData.version = cacheData.version; pluginData.fullPath = @@ -648,6 +648,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple if (pluginData.isPlugin) { pluginData.platformsData = data.platforms; pluginData.pluginVariables = data.variables; + pluginData.nativescript = data; } return pluginData; } catch (err) { diff --git a/package-lock.json b/package-lock.json index 1eafdfa37a..ee4a622caf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "nativescript", - "version": "9.0.0", + "version": "9.0.0-alpha.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nativescript", - "version": "9.0.0", + "version": "9.0.0-alpha.13", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@foxt/js-srp": "^0.0.3-patch2", + "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", + "@nativescript/hook": "3.0.4", "@npmcli/arborist": "9.1.6", "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", @@ -934,6 +935,16 @@ "node": ">=10" } }, + "node_modules/@nativescript/hook": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.4.tgz", + "integrity": "sha512-oahiN7V0D+fgl9o8mjGRgExujTpgSBB0DAFr3eX91qdlJZV8ywJ6mnvtHZyEI2j46yPgAE8jmNIw/Z/d3aWetw==", + "license": "Apache-2.0", + "dependencies": { + "glob": "^11.0.0", + "mkdirp": "^3.0.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 6d981cd88d..c9ecaeb6b4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nativescript", "main": "./lib/nativescript-cli-lib.js", - "version": "9.0.0", + "version": "9.0.0-alpha.14", "author": "NativeScript ", "description": "Command-line interface for building NativeScript projects", "bin": { @@ -54,8 +54,9 @@ "javascript" ], "dependencies": { - "@foxt/js-srp": "^0.0.3-patch2", + "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", + "@nativescript/hook": "3.0.4", "@npmcli/arborist": "9.1.6", "@rigor789/resolve-package-path": "1.0.7", "@nstudio/trapezedev-project": "7.2.3", diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 0ea539bfdd..85bc40641d 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -891,6 +891,7 @@ describe("Plugins service", () => { fullPath: pluginDir, isPlugin: true, platformsData: { android: "6.0.0", ios: "6.0.0" }, + nativescript: { platforms: { android: "6.0.0", ios: "6.0.0" } }, pluginVariables: undefined, }); }); @@ -1000,6 +1001,12 @@ describe("Plugins service", () => { android: "5.0.0", ios: "5.0.0", }, + nativescript: { + platforms: { + android: "5.0.0", + ios: "5.0.0", + }, + }, version: "6.3.2", }, { @@ -1011,6 +1018,12 @@ describe("Plugins service", () => { android: "6.2.0", ios: "6.2.0", }, + nativescript: { + platforms: { + android: "6.2.0", + ios: "6.2.0", + }, + }, version: "2.2.1", }, ], @@ -1072,6 +1085,12 @@ describe("Plugins service", () => { android: "6.0.0", ios: "6.0.0", }, + nativescript: { + platforms: { + android: "6.0.0", + ios: "6.0.0", + }, + }, version: "8.0.1", }, { @@ -1083,6 +1102,12 @@ describe("Plugins service", () => { android: "6.0.0", ios: "6.0.0", }, + nativescript: { + platforms: { + android: "6.0.0", + ios: "6.0.0", + }, + }, version: "4.0.0", }, ],