diff --git a/package-lock.json b/package-lock.json index 03d4812..5297d0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nativescript-angular", - "version": "20.1.1", + "version": "21.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nativescript-angular", - "version": "20.1.1", + "version": "21.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/packages/angular/src/lib/application.ts b/packages/angular/src/lib/application.ts index f4eb37d..8a8ceb5 100644 --- a/packages/angular/src/lib/application.ts +++ b/packages/angular/src/lib/application.ts @@ -1,3 +1,4 @@ +import * as AngularCore from '@angular/core'; import { ApplicationRef, EnvironmentProviders, NgModuleRef, NgZone, PlatformRef, Provider } from '@angular/core'; import { Application, @@ -18,6 +19,11 @@ import { NativeScriptLoadingService } from './loading.service'; import { APP_ROOT_VIEW, DISABLE_ROOT_VIEW_HANDLING, NATIVESCRIPT_ROOT_MODULE_ID } from './tokens'; import { NativeScriptDebug } from './trace'; +// Store the original @angular/core module for HMR +// This is crucial because HMR imports a fresh @angular/core with empty LView tracking +// We need to use the original one that has the registered LViews +(globalThis as any).__NS_ANGULAR_CORE__ = AngularCore; + export interface AppLaunchView extends LayoutBase { // called when the animation is to begin startAnimation?: () => void; @@ -236,16 +242,20 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { let launchEventDone = true; let targetRootView: View = null; const setRootView = (ref: NgModuleRef | ApplicationRef | View) => { + console.log('[ng-hmr] setRootView called, bootstrapId:', bootstrapId, 'ref type:', ref?.constructor?.name); if (bootstrapId === -1) { // treat edge cases + console.log('[ng-hmr] setRootView: bootstrapId is -1, returning early'); return; } if (ref instanceof NgModuleRef || ref instanceof ApplicationRef) { if (ref.injector.get(DISABLE_ROOT_VIEW_HANDLING, false)) { + console.log('[ng-hmr] setRootView: DISABLE_ROOT_VIEW_HANDLING is true, returning'); return; } } else { if (ref['__disable_root_view_handling']) { + console.log('[ng-hmr] setRootView: __disable_root_view_handling is true, returning'); return; } } @@ -253,6 +263,7 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { NativeScriptDebug.bootstrapLog(`Setting RootView ${launchEventDone ? 'outside of' : 'during'} launch event`); // TODO: check for leaks when root view isn't properly destroyed if (ref instanceof View) { + console.log('[ng-hmr] setRootView: ref is View, launchEventDone:', launchEventDone); if (NativeScriptDebug.isLogEnabled()) { NativeScriptDebug.bootstrapLog(`Setting RootView to ${ref}`); } @@ -267,14 +278,35 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { } const view = ref.injector.get(APP_ROOT_VIEW) as AppHostView | View; const newRoot = view instanceof AppHostView ? view.content : view; + console.log('[ng-hmr] setRootView: view from injector:', view?.constructor?.name, 'newRoot:', newRoot?.constructor?.name); + console.log('[ng-hmr] setRootView: launchEventDone:', launchEventDone, 'embedded:', options.embedded); if (NativeScriptDebug.isLogEnabled()) { NativeScriptDebug.bootstrapLog(`Setting RootView to ${newRoot}`); } if (options.embedded) { + console.log('[ng-hmr] setRootView: calling Application.run (embedded)'); Application.run({ create: () => newRoot }); } else if (launchEventDone) { + console.log('[ng-hmr] setRootView: calling Application.resetRootView'); + console.log('[ng-hmr] setRootView: newRoot details:', { + type: newRoot?.constructor?.name, + nativeView: !!newRoot?.nativeView, + parent: newRoot?.parent?.constructor?.name, + childCount: (newRoot as any)?.getChildrenCount?.() ?? 'N/A', + }); Application.resetRootView({ create: () => newRoot }); + console.log('[ng-hmr] setRootView: Application.resetRootView returned'); + // Check root view after reset + setTimeout(() => { + const currentRoot = Application.getRootView(); + console.log('[ng-hmr] setRootView: after reset, getRootView:', { + type: currentRoot?.constructor?.name, + nativeView: !!currentRoot?.nativeView, + childCount: (currentRoot as any)?.getChildrenCount?.() ?? 'N/A', + }); + }, 100); } else { + console.log('[ng-hmr] setRootView: setting targetRootView (launch in progress)'); targetRootView = newRoot; } }; @@ -286,8 +318,10 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { setRootView(errorTextBox); }; const bootstrapRoot = (reason: NgModuleReason) => { + console.log('[ng-hmr] bootstrapRoot called, reason:', reason); try { bootstrapId = Date.now(); + console.log('[ng-hmr] bootstrapRoot: new bootstrapId:', bootstrapId); const currentBootstrapId = bootstrapId; let bootstrapped = false; let onMainBootstrap = () => { @@ -297,20 +331,70 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { () => options.appModuleBootstrap(reason).then( (ref) => { + console.log('[ng-hmr] appModuleBootstrap resolved, ref:', ref?.constructor?.name); + console.log('[ng-hmr] currentBootstrapId:', currentBootstrapId, 'bootstrapId:', bootstrapId); if (currentBootstrapId !== bootstrapId) { // this module is old and not needed anymore // this may happen when developer uses async app initializer and the user exits the app before this bootstraps + console.log('[ng-hmr] bootstrap ID mismatch, destroying ref'); ref.destroy(); return; } mainModuleRef = ref; + + // Expose ApplicationRef for HMR to trigger change detection + // Check for ApplicationRef by duck-typing since instanceof can fail across module realms + const refAny = ref as any; + const isAppRef = refAny && typeof refAny.tick === 'function' && Array.isArray(refAny.components); + console.log('[ng-hmr] ref type check: isAppRef=', isAppRef, 'has tick=', typeof refAny?.tick === 'function', 'has components=', Array.isArray(refAny?.components)); + + if (isAppRef) { + global['__NS_ANGULAR_APP_REF__'] = ref; + // Mark boot complete for the HMR system + global['__NS_HMR_BOOT_COMPLETE__'] = true; + + // Register bootstrapped components for HMR lookup + if (!global['__NS_ANGULAR_COMPONENTS__']) { + global['__NS_ANGULAR_COMPONENTS__'] = {}; + } + // Get the component class from the first bootstrapped component + console.log('[ng-hmr] ApplicationRef components count:', refAny.components?.length); + if (refAny.components && refAny.components.length > 0) { + const componentRef = refAny.components[0]; + console.log('[ng-hmr] componentRef:', componentRef?.constructor?.name); + console.log('[ng-hmr] componentRef.componentType:', componentRef?.componentType?.name); + + // For Angular 17+ standalone components, the component type is on componentRef.componentType + // For older Angular, try componentRef.instance.constructor + let componentType = componentRef?.componentType; + if (!componentType && componentRef?.instance) { + componentType = componentRef.instance.constructor; + } + + if (componentType && componentType.name) { + global['__NS_ANGULAR_COMPONENTS__'][componentType.name] = componentType; + console.log('[ng-hmr] Registered component for HMR:', componentType.name); + } else { + console.log('[ng-hmr] Could not get componentType name'); + } + } else { + console.log('[ng-hmr] No components in ApplicationRef'); + } + } else { + const appRef = ref.injector.get(ApplicationRef, null); + if (appRef) { + global['__NS_ANGULAR_APP_REF__'] = appRef; + // Mark boot complete for the HMR system + global['__NS_HMR_BOOT_COMPLETE__'] = true; + } + } - (ref instanceof ApplicationRef ? ref.components[0] : ref).onDestroy( + (isAppRef ? refAny.components[0] : ref).onDestroy( () => (mainModuleRef = mainModuleRef === ref ? null : mainModuleRef), ); updatePlatformRef(ref, reason); const styleTag = ref.injector.get(NATIVESCRIPT_ROOT_MODULE_ID); - (ref instanceof ApplicationRef ? ref.components[0] : ref).onDestroy(() => { + (isAppRef ? refAny.components[0] : ref).onDestroy(() => { removeTaggedAdditionalCSS(styleTag); }); bootstrapped = true; @@ -457,30 +541,58 @@ export function runNativeScriptAngularApp(options: AppRunOptions) { if (oldAddEventListener) { global.NativeScriptGlobals.events.addEventListener = oldAddEventListener; } - if (import.meta['webpackHot']) { - // handle HMR Application.run - global['__dispose_app_ng_platform__'] = () => { - disposePlatform('hotreload'); - }; - global['__dispose_app_ng_modules__'] = () => { - disposeLastModules('hotreload'); - }; - global['__bootstrap_app_ng_modules__'] = () => { - bootstrapRoot('hotreload'); - }; - global['__cleanup_ng_hot__'] = () => { - Application.off(Application.launchEvent, launchCallback); - Application.off(Application.exitEvent, exitCallback); - disposeLastModules('hotreload'); + + // Detect HMR environment (webpack or Vite) + const isWebpackHot = !!import.meta['webpackHot']; + const isViteHot = !!import.meta['hot']; + const isHotReloadEnabled = isWebpackHot || isViteHot; + + // Always expose HMR globals for both webpack and Vite HMR support + // These allow the HMR runtime to properly dispose and re-bootstrap Angular + global['__dispose_app_ng_platform__'] = () => { + disposePlatform('hotreload'); + }; + global['__dispose_app_ng_modules__'] = () => { + disposeLastModules('hotreload'); + }; + global['__bootstrap_app_ng_modules__'] = () => { + bootstrapRoot('hotreload'); + }; + global['__cleanup_ng_hot__'] = () => { + Application.off(Application.launchEvent, launchCallback); + Application.off(Application.exitEvent, exitCallback); + disposeLastModules('hotreload'); + disposePlatform('hotreload'); + }; + global['__reboot_ng_modules__'] = (shouldDisposePlatform: boolean = false) => { + console.log('[ng-hmr] __reboot_ng_modules__ called, shouldDisposePlatform:', shouldDisposePlatform); + console.log('[ng-hmr] current bootstrapId:', bootstrapId, 'mainModuleRef:', !!mainModuleRef); + disposeLastModules('hotreload'); + console.log('[ng-hmr] after disposeLastModules, bootstrapId:', bootstrapId); + if (shouldDisposePlatform) { disposePlatform('hotreload'); - }; - global['__reboot_ng_modules__'] = (shouldDisposePlatform: boolean = false) => { - disposeLastModules('hotreload'); - if (shouldDisposePlatform) { - disposePlatform('hotreload'); - } - bootstrapRoot('hotreload'); - }; + } + console.log('[ng-hmr] calling bootstrapRoot...'); + bootstrapRoot('hotreload'); + console.log('[ng-hmr] bootstrapRoot returned, new bootstrapId:', bootstrapId); + }; + + if (isWebpackHot) { + // Webpack-specific HMR handling + import.meta['webpackHot'].decline(); + + if (!Application.hasLaunched()) { + Application.run(); + return; + } + bootstrapRoot('hotreload'); + return; + } + + if (isViteHot) { + // Vite-specific HMR handling + // Vite HMR is handled by @nativescript/vite's HMR runtime + // which will call __reboot_ng_modules__ when needed if (!Application.hasLaunched()) { Application.run(); diff --git a/packages/zone-js/dist/connectivity.ts b/packages/zone-js/dist/connectivity.ts deleted file mode 100644 index 8da4320..0000000 --- a/packages/zone-js/dist/connectivity.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable */ -import './core'; -import { Connectivity } from '@nativescript/core'; - -Zone.__load_patch('nativescript_connectivity', (global, zone, api) => { - api.patchMethod( - Connectivity, - 'startMonitoring', - (delegate, delegateName, name) => - function (self, args) { - const callback = args[0]; - return delegate.apply(self, [Zone.current.wrap(callback, 'NS Connectivity patch')]); - } - ); -}); diff --git a/packages/zone-js/dist/core.ts b/packages/zone-js/dist/core.ts deleted file mode 100644 index 8b0d1c0..0000000 --- a/packages/zone-js/dist/core.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable */ -import { patchClass, patchNativeScriptEventTarget } from './utils'; - -function isPropertyWritable(propertyDesc: any) { - if (!propertyDesc) { - return true; - } - - if (propertyDesc.writable === false) { - return false; - } - - return !(typeof propertyDesc.get === 'function' && typeof propertyDesc.set === 'undefined'); -} - -Zone.__load_patch('nativescript_patchMethod', (global, Zone, api) => { - api.patchMethod = function patchMethod(target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any): Function | null { - let proto = target; - while (proto && !proto.hasOwnProperty(name)) { - proto = Object.getPrototypeOf(proto); - } - if (!proto && target[name]) { - // somehow we did not find it, but we can see it. This happens on IE for Window properties. - proto = target; - } - - const delegateName = Zone.__symbol__(name); - let delegate: Function | null = null; - if (proto && !proto.hasOwnProperty(delegateName)) { - delegate = proto[delegateName] = proto[name]; - // check whether proto[name] is writable - // some property is readonly in safari, such as HtmlCanvasElement.prototype.toBlob - const desc = proto && api.ObjectGetOwnPropertyDescriptor(proto, name); - if (isPropertyWritable(desc)) { - const patchDelegate = patchFn(delegate!, delegateName, name); - proto[name] = function () { - return patchDelegate(this, arguments as any); - }; - api.attachOriginToPatched(proto[name], delegate); - // if (shouldCopySymbolProperties) { - // copySymbolProperties(delegate, proto[name]); - // } - } - } - return delegate; - }; -}); - -Zone.__load_patch('nativescript_event_target_api', (g, z, api: any) => { - api.patchNativeScriptEventTarget = patchNativeScriptEventTarget; -}); - -Zone.__load_patch('nativescript_patch_class_api', (g, z, api) => { - api.patchClass = (className: string) => patchClass(className, api); -}); - -// Initialize zone microtask queue on main thread -// TODO: dive into the ios runtime (PromiseProxy) and find a better solution -Promise.resolve().then(() => {}); diff --git a/packages/zone-js/dist/events.ts b/packages/zone-js/dist/events.ts deleted file mode 100644 index 824c5b6..0000000 --- a/packages/zone-js/dist/events.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable */ -import './core'; -import { Observable, View, Utils } from '@nativescript/core'; - -Zone.__load_patch('nativescript_observable_events', (g, z, api: any) => { - api.patchNativeScriptEventTarget(g, api, [Observable, Observable.prototype, View, View.prototype]); -}); - -Zone.__load_patch('nativescript_xhr_events', (g, z, api: any) => { - api.patchNativeScriptEventTarget(g, api, [XMLHttpRequest.prototype]); -}); - -// We're patching the Utils object instead of the actual js module -Zone.__load_patch('nativescript_mainThreadify', (global, zone, api) => { - api.patchMethod( - Utils, - 'mainThreadify', - (delegate, delegateName, name) => - function (self, args) { - const callback = args[0]; - return delegate.apply(self, [Zone.current.wrap(callback, 'NS mainThreadify patch')]); - } - ); -}); - -Zone.__load_patch('nativescript_executeOnMainThread', (global, zone, api) => { - api.patchMethod( - Utils, - 'executeOnMainThread', - (delegate, delegateName, name) => - function (self, args) { - const callback = args[0]; - return delegate.apply(self, [Zone.current.wrap(callback, 'NS executeOnMainThread patch')]); - } - ); -}); - -Zone.__load_patch('nativescript_dispatchToMainThread', (global, zone, api) => { - api.patchMethod( - Utils, - 'dispatchToMainThread', - (delegate, delegateName, name) => - function (self, args) { - const callback = args[0]; - return delegate.apply(self, [Zone.current.wrap(callback, 'NS dispatchToMainThread patch')]); - } - ); -}); - -Zone.__load_patch('nativescript_showModal', (global, zone, api) => { - api.patchMethod( - View.prototype, - 'showModal', - (delegate, delegateName, name) => - function (self, args) { - if (args.length === 2) { - const options = args[1]; - if (options.closeCallback) { - options.closeCallback = Zone.current.wrap(options.closeCallback, 'NS showModal patch'); - } - } else if (args.length > 3) { - args[3] = Zone.current.wrap(args[3], 'NS showModal patch'); - } - return delegate.apply(self, args); - } - ); -}); - -//! queueMacroTask should never be patched! We should consider it as a low level API to queue macroTasks which will be patched separately by other patches. diff --git a/packages/zone-js/dist/index.ts b/packages/zone-js/dist/index.ts deleted file mode 100644 index 330a1a5..0000000 --- a/packages/zone-js/dist/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './core'; -import './nativescript-globals'; -import './events'; -import './xhr'; -import './connectivity'; diff --git a/packages/zone-js/dist/nativescript-globals.ts b/packages/zone-js/dist/nativescript-globals.ts deleted file mode 100644 index 13efd5e..0000000 --- a/packages/zone-js/dist/nativescript-globals.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Zone.__load_patch('nativescript_MutationObserver', (global: any, Zone: ZoneType, api: _ZonePrivate) => { -// api.patchClass('MutationObserver'); -// api.patchClass('WebKitMutationObserver'); -// }); - -// Zone.__load_patch('nativescript_IntersectionObserver', (global: any, Zone: ZoneType, api: _ZonePrivate) => { -// api.patchClass('IntersectionObserver'); -// }); - -/* eslint-disable @typescript-eslint/no-explicit-any */ -Zone.__load_patch('nativescript_FileReader', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - const reader = global['FileReader']; - if (reader) { - reader.prototype.onload = reader.prototype.onload || null; - reader.prototype.onerror = reader.prototype.onerror || null; - reader.prototype.onabort = reader.prototype.onabort || null; - reader.prototype.onloadend = reader.prototype.onloadend || null; - reader.prototype.onloadstart = reader.prototype.onloadstart || null; - reader.prototype.onprogress = reader.prototype.onprogress || null; - } - api.patchClass('FileReader'); -}); diff --git a/packages/zone-js/dist/pre-zone-polyfills.ts b/packages/zone-js/dist/pre-zone-polyfills.ts deleted file mode 100644 index 54999d1..0000000 --- a/packages/zone-js/dist/pre-zone-polyfills.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const disabledPatches = [ - 'legacy', - 'EventTarget', - 'XHR', - 'MutationObserver', - 'IntersectionObserver', - 'FileReader', -]; - -for (const patch of disabledPatches) { - global[`__Zone_disable_${patch}`] = true; -} diff --git a/packages/zone-js/dist/trace-error.ts b/packages/zone-js/dist/trace-error.ts deleted file mode 100644 index c4a1fb2..0000000 --- a/packages/zone-js/dist/trace-error.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable */ -import { Trace } from '@nativescript/core'; - -Zone.__load_patch('nativescript_zone_to_trace_error', (global, zone, api) => { - zone[zone.__symbol__('unhandledPromiseRejectionHandler')] = (e) => { - Trace.error(e); - }; - zone[zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true; -}); diff --git a/packages/zone-js/dist/utils.ts b/packages/zone-js/dist/utils.ts deleted file mode 100644 index 84df0d6..0000000 --- a/packages/zone-js/dist/utils.ts +++ /dev/null @@ -1,437 +0,0 @@ -/* eslint-disable */ -const ZONE_SYMBOL_PREFIX = Zone.__symbol__(''); -const zoneSymbolEventNames: any = {}; -const ADD_EVENT_LISTENER_STR = 'addEventListener'; -const REMOVE_EVENT_LISTENER_STR = 'removeEventListener'; -function prepareEventNames(eventName: string, eventNameToString?: (eventName: string) => string) { - // const falseEventName = (eventNameToString ? eventNameToString(eventName) : eventName) + FALSE_STR; - // const trueEventName = (eventNameToString ? eventNameToString(eventName) : eventName) + TRUE_STR; - // const symbol = ZONE_SYMBOL_PREFIX + falseEventName; - // const symbolCapture = ZONE_SYMBOL_PREFIX + trueEventName; - // zoneSymbolEventNames[eventName] = {}; - // zoneSymbolEventNames[eventName][FALSE_STR] = symbol; - // zoneSymbolEventNames[eventName][TRUE_STR] = symbolCapture; - const symbol = ZONE_SYMBOL_PREFIX + (eventNameToString ? eventNameToString(eventName) : eventName) + 'false'; - zoneSymbolEventNames[eventName] = symbol; -} -interface NSTaskData { - thisArg?: any; - eventName?: string; - target?: any; - actualDelegate?: any; -} -interface ExtendedTaskData extends TaskData { - nsTaskData?: NSTaskData; -} - -interface ExtendedTask extends Task { - thisArg?: WeakRef; - eventName?: string; - target?: any; - customCallback?: any; - ranOnce?: boolean; -} -export interface PatchEventTargetOptions { - // validateHandler - vh?: (nativeDelegate: any, delegate: any, target: any, args: any) => boolean; - // addEventListener function name - add?: string; - // removeEventListener function name - rm?: string; - // once function name - once?: string; - // listeners function name - listeners?: string; - // removeAllListeners function name - rmAll?: string; - // check duplicate flag when addEventListener - chkDup?: boolean; - // return target flag when addEventListener - rt?: boolean; - // event compare handler - diff?: (task: any, delegate: any) => boolean; - // support passive or not - supportPassive?: boolean; - // get string from eventName (in nodejs, eventName maybe Symbol) - eventNameToString?: (eventName: any) => string; - // transfer eventName - transferEventName?: (eventName: string) => string; -} - -export function patchNativeScriptEventTarget(global: any, api: _ZonePrivate, apis?: any[], patchOptions?: PatchEventTargetOptions) { - const ADD_EVENT_LISTENER = (patchOptions && patchOptions.add) || ADD_EVENT_LISTENER_STR; - const REMOVE_EVENT_LISTENER = (patchOptions && patchOptions.rm) || REMOVE_EVENT_LISTENER_STR; - const ONCE = (patchOptions && patchOptions.once) || 'once'; - - const zoneSymbolAddEventListener = Zone.__symbol__(ADD_EVENT_LISTENER); - - const ADD_EVENT_LISTENER_SOURCE = '.' + ADD_EVENT_LISTENER + ':'; - - function patchNativeScriptEventTargetMethods(obj, patchOptions) { - if (!obj) { - return false; - } - const eventNameToString = patchOptions && patchOptions.eventNameToString; - // let proto = obj; - // while (proto && !proto.hasOwnProperty(ADD_EVENT_LISTENER)) { - // proto = Object.getPrototypeOf(proto); - // } - // if (!proto && obj[ADD_EVENT_LISTENER]) { - // // somehow we did not find it, but we can see it. This happens on IE for Window properties. - // proto = obj; - // } - - // if (!proto) { - // return false; - // } - // if (proto[zoneSymbolAddEventListener]) { - // return false; - // } - function compare(task: ExtendedTask, delegate: any, thisArg?: any) { - const taskThis = task.thisArg ? task.thisArg.get() : undefined; - if (!thisArg) { - thisArg = undefined; // keep consistent - } - return task.callback === delegate && taskThis === thisArg; - } - - const nativeAddListener = api.patchMethod( - obj, - ADD_EVENT_LISTENER, - (delegate, delegateName, name) => - function (originalTarget, originalArgs) { - const addSingleEvent = function (target, args) { - const eventName = args[0]; - const callback = args[1]; - const taskData: NSTaskData = {}; - const thisArg = (args.length > 1 && args[2]) || undefined; - taskData.target = target; - taskData.eventName = eventName; - taskData.thisArg = thisArg; - let symbolEventNames = zoneSymbolEventNames[eventName]; - if (!symbolEventNames) { - prepareEventNames(eventName, eventNameToString); - symbolEventNames = zoneSymbolEventNames[eventName]; - } - const symbolEventName = symbolEventNames; - let existingTasks = target[symbolEventName]; - let isExisting = false; - let checkDuplicate = false; - if (existingTasks) { - // already have task registered - isExisting = true; - if (checkDuplicate) { - for (let i = 0; i < existingTasks.length; i++) { - if (compare(existingTasks[i], delegate, taskData.thisArg)) { - // same callback, same capture, same event name, just return - return; - } - } - } - } else { - existingTasks = target[symbolEventName] = []; - } - const schedule = (task: Task) => { - const args2 = [taskData.eventName, task.invoke]; - if (taskData.thisArg) { - args2.push(taskData.thisArg); - } - delegate.apply(target, args2); - }; - const unschedule = (task: ExtendedTask) => { - const args2 = [task.eventName, task.invoke]; - if (task.thisArg) { - args2.push(task.thisArg.get()); - } - nativeRemoveListener.apply(target, args2); - }; - const data: ExtendedTaskData = { - nsTaskData: taskData, - }; - const objName = obj.name || obj?.constructor?.name; - const task: ExtendedTask = Zone.current.scheduleEventTask(objName + ':' + (eventNameToString ? eventNameToString(eventName) : eventName), callback, data, schedule, unschedule); - // should clear taskData.target to avoid memory leak - // issue, https://github.com/angular/angular/issues/20442 - taskData.target = null; - - // need to clear up taskData because it is a global object - if (data) { - data.nsTaskData = null; - } - task.target = target; - // task.capture = capture; - task.thisArg = (thisArg && new WeakRef(thisArg)) || undefined; - task.eventName = eventName; - existingTasks.push(task); - // return nativeAddListener.apply(target, args); - }; - const events: string[] = typeof originalArgs[0] === 'string' ? originalArgs[0].split(',') : []; - if (events.length > 0) { - Array.prototype.splice.call(originalArgs, 0, 1); - for (let i = 0; i < events.length; i++) { - addSingleEvent(originalTarget, [events[i].trim(), ...originalArgs]); - } - } else { - addSingleEvent(originalTarget, originalArgs); - } - } - ); - - const nativeOnce = api.patchMethod( - obj, - ONCE, - (delegate, delegateName, name) => - function (originalTarget, originalArgs) { - const addSingleEvent = function (target, args) { - const eventName = args[0]; - const callback = args[1]; - const taskData: NSTaskData = {}; - const thisArg = (args.length > 1 && args[2]) || undefined; - taskData.target = target; - taskData.eventName = eventName; - taskData.thisArg = thisArg; - let symbolEventNames = zoneSymbolEventNames[eventName]; - if (!symbolEventNames) { - prepareEventNames(eventName, eventNameToString); - symbolEventNames = zoneSymbolEventNames[eventName]; - } - const symbolEventName = symbolEventNames; - let existingTasks = target[symbolEventName]; - let isExisting = false; - let checkDuplicate = false; - if (existingTasks) { - // already have task registered - isExisting = true; - if (checkDuplicate) { - for (let i = 0; i < existingTasks.length; i++) { - if (compare(existingTasks[i], delegate, taskData.thisArg)) { - // same callback, same capture, same event name, just return - return; - } - } - } - } else { - existingTasks = target[symbolEventName] = []; - } - const schedule = (task: ExtendedTask) => { - task.ranOnce = false; - task.customCallback = function (...args) { - task.invoke.apply(this, args); - task.ranOnce = true; - task.target[REMOVE_EVENT_LISTENER](task.eventName, task.callback, task.thisArg ? task.thisArg.get() : undefined); - }; - const args2 = [taskData.eventName, task.invoke]; - if (taskData.thisArg) { - args2.push(taskData.thisArg); - } - delegate.apply(target, args2); - }; - const unschedule = (task: ExtendedTask) => { - if (task.ranOnce) { - return; - } - const args2 = [task.eventName, task.invoke]; - if (task.thisArg) { - args2.push(task.thisArg.get()); - } - nativeRemoveListener.apply(target, args2); - }; - const data: ExtendedTaskData = { - nsTaskData: taskData, - }; - const objName = obj.name || obj?.constructor?.name; - const task: ExtendedTask = Zone.current.scheduleEventTask(objName + ':' + (eventNameToString ? eventNameToString(eventName) : eventName), callback, data, schedule, unschedule); - // should clear taskData.target to avoid memory leak - // issue, https://github.com/angular/angular/issues/20442 - taskData.target = null; - - // need to clear up taskData because it is a global object - if (data) { - data.nsTaskData = null; - } - task.target = target; - // task.capture = capture; - task.thisArg = (thisArg && new WeakRef(thisArg)) || undefined; - task.eventName = eventName; - existingTasks.push(task); - }; - const events: string[] = originalArgs && Array.isArray(originalArgs) && typeof originalArgs[0] === 'string' ? originalArgs[0].split(',') : []; - if (events.length > 0) { - if (originalArgs && Array.isArray(originalArgs)) { - originalArgs.splice(0, 1); - } - for (let i = 0; i < events.length; i++) { - addSingleEvent(originalTarget, [events[i].trim(), ...originalArgs]); - } - } else { - addSingleEvent(originalTarget, originalArgs); - } - } - ); - - const nativeRemoveListener = api.patchMethod( - obj, - REMOVE_EVENT_LISTENER, - (delegate, delegateName, name) => - function (originalTarget, originalArgs) { - const removeSingleEvent = function (target, args) { - const eventName = args[0]; - const callback = args[1]; - const thisArg = (args.length > 1 && args[2]) || undefined; - const symbolEventNames = zoneSymbolEventNames[eventName]; - const symbolEventName = symbolEventNames; - const existingTasks: Task[] = symbolEventName && target[symbolEventName]; - const removeAll = !callback; // object.off(event); - if (existingTasks) { - if (removeAll) { - target[symbolEventName] = null; - } - for (let i = 0; i < existingTasks.length; i++) { - const existingTask = existingTasks[i]; - if (removeAll) { - (existingTask as any).isRemoved = true; - (existingTask as any).allRemoved = true; - existingTask.zone.cancelTask(existingTask); - continue; - } - if (compare(existingTask, callback, thisArg)) { - existingTasks.splice(i, 1); - // set isRemoved to data for faster invokeTask check - (existingTask as any).isRemoved = true; - if (existingTasks.length === 0) { - // all tasks for the eventName + capture have gone, - // remove globalZoneAwareCallback and remove the task cache from target - (existingTask as any).allRemoved = true; - target[symbolEventName] = null; - } - existingTask.zone.cancelTask(existingTask); - return; - } - } - } - return nativeRemoveListener.apply(target, args); - }; - const events: string[] = typeof originalArgs[0] === 'string' ? originalArgs[0].split(',') : []; - if (events.length > 0) { - Array.prototype.splice.call(originalArgs, 0, 1); - for (let i = 0; i < events.length; i++) { - removeSingleEvent(originalTarget, [events[i].trim(), ...originalArgs]); - } - } else { - removeSingleEvent(originalTarget, originalArgs); - } - } - ); - } - - let results: any[] = []; - for (let i = 0; i < apis.length; i++) { - results[i] = patchNativeScriptEventTargetMethods(apis[i], patchOptions); - } - - return results; -} - -const _global = global; - -const zoneSymbol = Zone.__symbol__; - -const originalInstanceKey = zoneSymbol('originalInstance'); - -function getAllPropertyNames(obj: unknown) { - const props = new Set(); - - do { - Object.getOwnPropertyNames(obj).forEach((prop) => { - props.add(prop); - }); - } while ((obj = Object.getPrototypeOf(obj)) && obj !== Object.prototype); - - return Array.from(props); -} - -// wrap some native API on `window` -export function patchClass(className: string, api: _ZonePrivate) { - const OriginalClass = _global[className]; - if (!OriginalClass) return; - // keep original class in global - _global[zoneSymbol(className)] = OriginalClass; - - _global[className] = function () { - const a = api.bindArguments(arguments, className); - switch (a.length) { - case 0: - this[originalInstanceKey] = new OriginalClass(); - break; - case 1: - this[originalInstanceKey] = new OriginalClass(a[0]); - break; - case 2: - this[originalInstanceKey] = new OriginalClass(a[0], a[1]); - break; - case 3: - this[originalInstanceKey] = new OriginalClass(a[0], a[1], a[2]); - break; - case 4: - this[originalInstanceKey] = new OriginalClass(a[0], a[1], a[2], a[3]); - break; - default: - throw new Error('Arg list too long.'); - } - }; - - // attach original delegate to patched function - api.attachOriginToPatched(_global[className], OriginalClass); - - const instance = new OriginalClass(function () {}); - - let prop; - for (prop of getAllProperties(instance)) { - // https://bugs.webkit.org/show_bug.cgi?id=44721 - if (className === 'XMLHttpRequest' && prop === 'responseBlob') continue; - (function (prop) { - if (typeof instance[prop] === 'function') { - _global[className].prototype[prop] = function () { - return this[originalInstanceKey][prop].apply(this[originalInstanceKey], arguments); - }; - } else { - api.ObjectDefineProperty(_global[className].prototype, prop, { - set: function (fn) { - if (typeof fn === 'function') { - this[originalInstanceKey][prop] = api.wrapWithCurrentZone(fn, className + '.' + prop); - // keep callback in wrapped function so we can - // use it in Function.prototype.toString to return - // the native one. - api.attachOriginToPatched(this[originalInstanceKey][prop], fn); - } else { - this[originalInstanceKey][prop] = fn; - } - }, - get: function () { - return this[originalInstanceKey][prop]; - }, - }); - } - })(prop); - } - - for (prop of Object.getOwnPropertyNames(OriginalClass)) { - if (prop !== 'prototype' && OriginalClass.hasOwnProperty(prop) && isWritable(_global[className], prop)) { - _global[className][prop] = OriginalClass[prop]; - } - } -} - -function getAllProperties(toCheck: any, lastProto = Object.prototype) { - const props = []; - let obj = toCheck; - do { - props.push(...Object.getOwnPropertyNames(obj)); - } while ((obj = Object.getPrototypeOf(obj)) && obj !== lastProto); - - return Array.from(new Set(props)); -} - -function isWritable(obj: T, key: keyof T) { - return Object.getOwnPropertyDescriptor(obj, key)?.writable ?? true; -} diff --git a/packages/zone-js/dist/xhr.ts b/packages/zone-js/dist/xhr.ts deleted file mode 100644 index 2e6c13a..0000000 --- a/packages/zone-js/dist/xhr.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* eslint-disable */ -Zone.__load_patch('nativescript_XHR', (global: any, Zone: ZoneType, api: _ZonePrivate) => { - const ADD_EVENT_LISTENER_STR = 'addEventListener'; - /** removeEventListener string const */ - const REMOVE_EVENT_LISTENER_STR = 'removeEventListener'; - /** zoneSymbol addEventListener */ - const ZONE_SYMBOL_ADD_EVENT_LISTENER = Zone.__symbol__(ADD_EVENT_LISTENER_STR); - /** zoneSymbol removeEventListener */ - const ZONE_SYMBOL_REMOVE_EVENT_LISTENER = Zone.__symbol__(REMOVE_EVENT_LISTENER_STR); - /** true string const */ - const TRUE_STR = 'true'; - /** false string const */ - const FALSE_STR = 'false'; - /** Zone symbol prefix string const. */ - const ZONE_SYMBOL_PREFIX = Zone.__symbol__(''); - const zoneSymbol = Zone.__symbol__; - function scheduleMacroTaskWithCurrentZone(source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void, customCancel?: (task: Task) => void): MacroTask { - return Zone.current.scheduleMacroTask(source, callback, data, customSchedule, customCancel); - } - // Treat XMLHttpRequest as a macrotask. - patchXHR(global); - - const XHR_TASK = zoneSymbol('xhrTask'); - const XHR_SYNC = zoneSymbol('xhrSync'); - const XHR_LISTENER = zoneSymbol('xhrListener'); - const XHR_SCHEDULED = zoneSymbol('xhrScheduled'); - const XHR_URL = zoneSymbol('xhrURL'); - const XHR_ERROR_BEFORE_SCHEDULED = zoneSymbol('xhrErrorBeforeScheduled'); - - interface XHROptions extends TaskData { - target: any; - url: string; - args: any[]; - aborted: boolean; - } - - function patchXHR(window: any) { - const XMLHttpRequest = window['XMLHttpRequest']; - if (!XMLHttpRequest) { - // XMLHttpRequest is not available in service worker - return; - } - const XMLHttpRequestPrototype: any = XMLHttpRequest.prototype; - - function findPendingTask(target: any) { - return target[XHR_TASK]; - } - - let oriAddListener = XMLHttpRequestPrototype[ZONE_SYMBOL_ADD_EVENT_LISTENER]; - let oriRemoveListener = XMLHttpRequestPrototype[ZONE_SYMBOL_REMOVE_EVENT_LISTENER]; - if (!oriAddListener) { - const XMLHttpRequestEventTarget = window['XMLHttpRequestEventTarget']; - if (XMLHttpRequestEventTarget) { - const XMLHttpRequestEventTargetPrototype = XMLHttpRequestEventTarget.prototype; - oriAddListener = XMLHttpRequestEventTargetPrototype[ZONE_SYMBOL_ADD_EVENT_LISTENER]; - oriRemoveListener = XMLHttpRequestEventTargetPrototype[ZONE_SYMBOL_REMOVE_EVENT_LISTENER]; - } - } - - const READY_STATE_CHANGE = 'readystatechange'; - const SCHEDULED = 'scheduled'; - - function scheduleTask(task: Task) { - const data = task.data; - const target = data.target; - target[XHR_SCHEDULED] = false; - target[XHR_ERROR_BEFORE_SCHEDULED] = false; - // remove existing event listener - const listener = target[XHR_LISTENER]; - if (!oriAddListener) { - oriAddListener = target[ZONE_SYMBOL_ADD_EVENT_LISTENER]; - oriRemoveListener = target[ZONE_SYMBOL_REMOVE_EVENT_LISTENER]; - } - - if (listener) { - oriRemoveListener.call(target, READY_STATE_CHANGE, listener); - } - const newListener = (target[XHR_LISTENER] = () => { - if (target.readyState === target.DONE) { - // sometimes on some browsers XMLHttpRequest will fire onreadystatechange with - // readyState=4 multiple times, so we need to check task state here - if (!data.aborted && target[XHR_SCHEDULED] && task.state === SCHEDULED) { - // check whether the xhr has registered onload listener - // if that is the case, the task should invoke after all - // onload listeners finish. - // Also if the request failed without response (status = 0), the load event handler - // will not be triggered, in that case, we should also invoke the placeholder callback - // to close the XMLHttpRequest::send macroTask. - // https://github.com/angular/angular/issues/38795 - const loadTasks = target[Zone.__symbol__('loadfalse')]; - if (target.status !== 0 && loadTasks && loadTasks.length > 0) { - const oriInvoke = task.invoke; - task.invoke = function () { - // need to load the tasks again, because in other - // load listener, they may remove themselves - const loadTasks = target[Zone.__symbol__('loadfalse')]; - for (let i = 0; i < loadTasks.length; i++) { - if (loadTasks[i] === task) { - loadTasks.splice(i, 1); - } - } - if (!data.aborted && task.state === SCHEDULED) { - oriInvoke.call(task); - } - }; - loadTasks.push(task); - } else { - task.invoke(); - } - } else if (!data.aborted && target[XHR_SCHEDULED] === false) { - // error occurs when xhr.send() - target[XHR_ERROR_BEFORE_SCHEDULED] = true; - } - } - }); - oriAddListener.call(target, READY_STATE_CHANGE, newListener); - - const storedTask: Task = target[XHR_TASK]; - if (!storedTask) { - target[XHR_TASK] = task; - } - sendNative!.apply(target, data.args); - target[XHR_SCHEDULED] = true; - return task; - } - - function placeholderCallback() {} - - function clearTask(task: Task) { - const data = task.data; - // Note - ideally, we would call data.target.removeEventListener here, but it's too late - // to prevent it from firing. So instead, we store info for the event listener. - data.aborted = true; - return abortNative!.apply(data.target, data.args); - } - - const openNative = api.patchMethod( - XMLHttpRequestPrototype, - 'open', - () => - function (self: any, args: any[]) { - self[XHR_SYNC] = args[2] == false; - self[XHR_URL] = args[1]; - return openNative!.apply(self, args); - } - ); - - const XMLHTTPREQUEST_SOURCE = 'XMLHttpRequest.send'; - const fetchTaskAborting = zoneSymbol('fetchTaskAborting'); - const fetchTaskScheduling = zoneSymbol('fetchTaskScheduling'); - const sendNative: Function | null = api.patchMethod( - XMLHttpRequestPrototype, - 'send', - () => - function (self: any, args: any[]) { - if ((Zone.current as any)[fetchTaskScheduling] === true) { - // a fetch is scheduling, so we are using xhr to polyfill fetch - // and because we already schedule macroTask for fetch, we should - // not schedule a macroTask for xhr again - return sendNative!.apply(self, args); - } - if (self[XHR_SYNC]) { - // if the XHR is sync there is no task to schedule, just execute the code. - return sendNative!.apply(self, args); - } else { - const options: XHROptions = { target: self, url: self[XHR_URL], isPeriodic: false, args: args, aborted: false }; - const task = scheduleMacroTaskWithCurrentZone(XMLHTTPREQUEST_SOURCE, placeholderCallback, options, scheduleTask, clearTask); - if (self && self[XHR_ERROR_BEFORE_SCHEDULED] === true && !options.aborted && task.state === SCHEDULED) { - // xhr request throw error when send - // we should invoke task instead of leaving a scheduled - // pending macroTask - task.invoke(); - } - } - } - ); - - const abortNative = api.patchMethod( - XMLHttpRequestPrototype, - 'abort', - () => - function (self: any, args: any[]) { - const task: Task = findPendingTask(self); - if (task && typeof task.type == 'string') { - // If the XHR has already completed, do nothing. - // If the XHR has already been aborted, do nothing. - // Fix #569, call abort multiple times before done will cause - // macroTask task count be negative number - if (task.cancelFn == null || (task.data && (task.data).aborted)) { - return; - } - task.zone.cancelTask(task); - } else if ((Zone.current as any)[fetchTaskAborting] === true) { - // the abort is called from fetch polyfill, we need to call native abort of XHR. - return abortNative!.apply(self, args); - } - // Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no - // task - // to cancel. Do nothing. - } - ); - } -});