Skip to content
24,908 changes: 14,223 additions & 10,685 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nativescript/angular",
"version": "21.0.0",
"version": "21.0.1-alpha.5",
"homepage": "https://nativescript.org/",
"repository": {
"type": "git",
Expand Down
314 changes: 268 additions & 46 deletions packages/angular/src/lib/application.ts

Large diffs are not rendered by default.

121 changes: 79 additions & 42 deletions packages/angular/src/lib/element-registry/common-views.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,86 @@
import { AbsoluteLayout, ActivityIndicator, Button, ContentView, DatePicker, DockLayout, FlexboxLayout, FormattedString, Frame, GridLayout, HtmlView, Image, Label, ListPicker, ListView, Page, Placeholder, Progress, ProxyViewContainer, Repeater, RootLayout, ScrollView, SearchBar, SegmentedBar, SegmentedBarItem, Slider, Span, SplitView, StackLayout, Switch, TabView, TextField, TextView, TimePicker, WebView, WrapLayout } from '@nativescript/core';
import {
AbsoluteLayout,
ActivityIndicator,
Button,
ContentView,
DatePicker,
DockLayout,
FlexboxLayout,
FormattedString,
Frame,
GridLayout,
HtmlView,
Image,
Label,
ListPicker,
ListView,
Page,
Placeholder,
Progress,
ProxyViewContainer,
Repeater,
RootLayout,
ScrollView,
SearchBar,
SegmentedBar,
SegmentedBarItem,
Slider,
Span,
SplitView,
StackLayout,
Switch,
TabView,
TextField,
TextView,
TimePicker,
WebView,
WrapLayout,
} from '@nativescript/core';
import { formattedStringMeta, frameMeta, textBaseMeta } from './metas';
import { registerElement } from './registry';

// Register default NativeScript components
// Note: ActionBar related components are registerd together with action-bar directives.
export function registerNativeScriptViewComponents() {
if (!(<any>global).__ngRegisteredViews) {
(<any>global).__ngRegisteredViews = true;
registerElement('AbsoluteLayout', () => AbsoluteLayout);
registerElement('ActivityIndicator', () => ActivityIndicator);
registerElement('Button', () => Button, textBaseMeta);
registerElement('ContentView', () => ContentView);
registerElement('DatePicker', () => DatePicker);
registerElement('DockLayout', () => DockLayout);
registerElement('Frame', () => Frame, frameMeta);
registerElement('GridLayout', () => GridLayout);
registerElement('HtmlView', () => HtmlView);
registerElement('Image', () => Image);
// Parse5 changes <Image> tags to <img>. WTF!
registerElement('img', () => Image);
registerElement('Label', () => Label, textBaseMeta);
registerElement('ListPicker', () => ListPicker);
registerElement('ListView', () => ListView);
registerElement('Page', () => Page);
registerElement('Placeholder', () => Placeholder);
registerElement('Progress', () => Progress);
registerElement('ProxyViewContainer', () => ProxyViewContainer);
registerElement('Repeater', () => Repeater);
registerElement('RootLayout', () => RootLayout);
registerElement('ScrollView', () => ScrollView);
registerElement('SearchBar', () => SearchBar);
registerElement('SegmentedBar', () => SegmentedBar);
registerElement('SegmentedBarItem', () => SegmentedBarItem);
registerElement('Slider', () => Slider);
registerElement('SplitView', () => SplitView);
registerElement('StackLayout', () => StackLayout);
registerElement('FlexboxLayout', () => FlexboxLayout);
registerElement('Switch', () => Switch);
registerElement('TabView', () => TabView);
registerElement('TextField', () => TextField, textBaseMeta);
registerElement('TextView', () => TextView, textBaseMeta);
registerElement('TimePicker', () => TimePicker);
registerElement('WebView', () => WebView);
registerElement('WrapLayout', () => WrapLayout);
registerElement('FormattedString', () => FormattedString, formattedStringMeta);
registerElement('Span', () => Span);
}
// No guard needed — registerElement calls Map.set which is idempotent.
// The old `elementMap.size > 0` guard could falsely skip registration
// in Vite HMR mode when elements were registered by a prior boot phase.
registerElement('AbsoluteLayout', () => AbsoluteLayout);
registerElement('ActivityIndicator', () => ActivityIndicator);
registerElement('Button', () => Button, textBaseMeta);
registerElement('ContentView', () => ContentView);
registerElement('DatePicker', () => DatePicker);
registerElement('DockLayout', () => DockLayout);
registerElement('Frame', () => Frame, frameMeta);
registerElement('GridLayout', () => GridLayout);
registerElement('HtmlView', () => HtmlView);
registerElement('Image', () => Image);
// Parse5 changes <Image> tags to <img>. WTF!
registerElement('img', () => Image);
registerElement('Label', () => Label, textBaseMeta);
registerElement('ListPicker', () => ListPicker);
registerElement('ListView', () => ListView);
registerElement('Page', () => Page);
registerElement('Placeholder', () => Placeholder);
registerElement('Progress', () => Progress);
registerElement('ProxyViewContainer', () => ProxyViewContainer);
registerElement('Repeater', () => Repeater);
registerElement('RootLayout', () => RootLayout);
registerElement('ScrollView', () => ScrollView);
registerElement('SearchBar', () => SearchBar);
registerElement('SegmentedBar', () => SegmentedBar);
registerElement('SegmentedBarItem', () => SegmentedBarItem);
registerElement('Slider', () => Slider);
registerElement('SplitView', () => SplitView);
registerElement('StackLayout', () => StackLayout);
registerElement('FlexboxLayout', () => FlexboxLayout);
registerElement('Switch', () => Switch);
registerElement('TabView', () => TabView);
registerElement('TextField', () => TextField, textBaseMeta);
registerElement('TextView', () => TextView, textBaseMeta);
registerElement('TimePicker', () => TimePicker);
registerElement('WebView', () => WebView);
registerElement('WrapLayout', () => WrapLayout);
registerElement('FormattedString', () => FormattedString, formattedStringMeta);
registerElement('Span', () => Span);
}
6 changes: 5 additions & 1 deletion packages/angular/src/lib/element-registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { ViewClassMeta } from '../views/view-types';

export type ViewResolver = () => any;

export const elementMap = new Map<string, { resolver: ViewResolver; meta?: ViewClassMeta }>();
// Use a global elementMap so the vendor bundle and HTTP-loaded module instances
// share the same element registry during Vite HMR (where two copies of
// @nativescript/angular can coexist in separate module realms).
export const elementMap: Map<string, { resolver: ViewResolver; meta?: ViewClassMeta }> =
(globalThis as any).__NS_NG_ELEMENT_MAP__ || ((globalThis as any).__NS_NG_ELEMENT_MAP__ = new Map());
const camelCaseSplit = /([a-z0-9])([A-Z])/g;
const defaultViewMeta: ViewClassMeta = { skipAddToDom: false };

Expand Down
76 changes: 76 additions & 0 deletions packages/angular/src/lib/hmr-compiled-components-core.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
getAngularCoreForHmrReset,
rememberAngularCoreForHmr,
resetAngularHmrCompiledComponents,
setAngularCoreForHmr,
} from './hmr-compiled-components-core';

describe('Angular HMR compiled component reset', () => {
it('calls Angular internal compiled-component reset when available', () => {
const core = {
ɵresetCompiledComponents: jest.fn(),
};

expect(resetAngularHmrCompiledComponents(core)).toBe(true);
expect(core.ɵresetCompiledComponents).toHaveBeenCalledTimes(1);
});

it('returns false when Angular core does not expose the reset hook', () => {
expect(resetAngularHmrCompiledComponents({})).toBe(false);
});

it('swallows reset failures so HMR disposal can continue', () => {
const core = {
ɵresetCompiledComponents: jest.fn(() => {
throw new Error('boom');
}),
};

expect(resetAngularHmrCompiledComponents(core)).toBe(false);
expect(core.ɵresetCompiledComponents).toHaveBeenCalledTimes(1);
});

it('prefers the preserved global Angular core object for resets', () => {
const originalCore = {
ɵresetCompiledComponents: jest.fn(),
};
const replacementCore = {
ɵresetCompiledComponents: jest.fn(),
};
const globalObj: any = {
__NS_ANGULAR_CORE__: originalCore,
};

expect(getAngularCoreForHmrReset(replacementCore, globalObj)).toBe(originalCore);
});

it('remembers the first Angular core object and does not replace it later', () => {
const originalCore = {
ɵresetCompiledComponents: jest.fn(),
};
const replacementCore = {
ɵresetCompiledComponents: jest.fn(),
};
const globalObj: any = {};

expect(rememberAngularCoreForHmr(originalCore, globalObj)).toBe(originalCore);
expect(globalObj.__NS_ANGULAR_CORE__).toBe(originalCore);
expect(rememberAngularCoreForHmr(replacementCore, globalObj)).toBe(originalCore);
expect(globalObj.__NS_ANGULAR_CORE__).toBe(originalCore);
});

it('allows the active Angular core realm to be updated explicitly for HMR resets', () => {
const originalCore = {
ɵresetCompiledComponents: jest.fn(),
};
const replacementCore = {
ɵresetCompiledComponents: jest.fn(),
};
const globalObj: any = {
__NS_ANGULAR_CORE__: originalCore,
};

expect(setAngularCoreForHmr(replacementCore, globalObj)).toBe(replacementCore);
expect(globalObj.__NS_ANGULAR_CORE__).toBe(replacementCore);
});
});
52 changes: 52 additions & 0 deletions packages/angular/src/lib/hmr-compiled-components-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
type AngularCoreWithCompiledComponentReset = {
ɵresetCompiledComponents?: () => void;
};

type AngularCoreHolder = {
__NS_ANGULAR_CORE__?: AngularCoreWithCompiledComponentReset | null;
};

export function setAngularCoreForHmr(
core: AngularCoreWithCompiledComponentReset | null | undefined,
globalObj: AngularCoreHolder = globalThis as AngularCoreHolder,
): AngularCoreWithCompiledComponentReset | null | undefined {
if (core) {
globalObj.__NS_ANGULAR_CORE__ = core;
}

return getAngularCoreForHmrReset(core, globalObj);
}

export function getAngularCoreForHmrReset(
core: AngularCoreWithCompiledComponentReset | null | undefined,
globalObj: AngularCoreHolder = globalThis as AngularCoreHolder,
): AngularCoreWithCompiledComponentReset | null | undefined {
return globalObj.__NS_ANGULAR_CORE__ || core;
}

export function rememberAngularCoreForHmr(
core: AngularCoreWithCompiledComponentReset | null | undefined,
globalObj: AngularCoreHolder = globalThis as AngularCoreHolder,
): AngularCoreWithCompiledComponentReset | null | undefined {
if (!globalObj.__NS_ANGULAR_CORE__ && core) {
globalObj.__NS_ANGULAR_CORE__ = core;
}

return getAngularCoreForHmrReset(core, globalObj);
}

export function resetAngularHmrCompiledComponents(
core: AngularCoreWithCompiledComponentReset | null | undefined,
): boolean {
const resetCompiledComponents = core?.ɵresetCompiledComponents;
if (typeof resetCompiledComponents !== 'function') {
return false;
}

try {
resetCompiledComponents.call(core);
return true;
} catch {
return false;
}
}
55 changes: 55 additions & 0 deletions packages/angular/src/lib/legacy/router/hmr-route-bootstrap-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
type AngularBootstrapRouteLike = {
children?: AngularBootstrapRouteLike[];
};

function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') {
return false;
}

const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}

function shouldStripRouteKey(key: string): boolean {
return key.startsWith('_') || key.startsWith('ɵ');
}

function cloneRouteValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.slice();
}

if (isPlainObject(value)) {
return { ...value };
}

return value;
}

function cloneBootstrapRoute<T extends object>(route: T): T {
const next: AngularBootstrapRouteLike = {};

for (const [key, value] of Object.entries(route as Record<string, unknown>)) {
if (shouldStripRouteKey(key)) {
continue;
}

if (key === 'children' && Array.isArray(value)) {
next.children = cloneRoutesForBootstrap(value);
continue;
}

next[key] = cloneRouteValue(value);
}

return next as T;
}

export function cloneRoutesForBootstrap<T extends object>(routes: T[] | undefined | null): T[] {
if (!Array.isArray(routes)) {
return [];
}

return routes.map((route) => cloneBootstrapRoute(route));
}
58 changes: 58 additions & 0 deletions packages/angular/src/lib/legacy/router/hmr-route-bootstrap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { cloneRoutesForBootstrap } from './hmr-route-bootstrap-core';

describe('cloneRoutesForBootstrap', () => {
it('drops private Angular router cache fields while preserving public route config', () => {
const loadComponent = jest.fn();
const canActivate = [jest.fn()];
const routes = [
{
path: 'signup-landing',
loadComponent,
canActivate,
data: { source: 'signup' },
_loadedComponent: { stale: true },
_loadedInjector: { stale: true },
_loadedRoutes: [{ stale: true }],
_injector: { stale: true },
_loadedNgModuleFactory: { stale: true },
ɵrouterPageId: 'stale',
children: [
{
path: 'child',
loadChildren: jest.fn(),
_loadedComponent: { nested: true },
},
],
},
] as any;

const cloned = cloneRoutesForBootstrap(routes);

expect(cloned).not.toBe(routes);
expect(cloned[0]).not.toBe(routes[0]);
expect(cloned[0].loadComponent).toBe(loadComponent);
expect(cloned[0].canActivate).toEqual(canActivate);
expect(cloned[0].canActivate).not.toBe(canActivate);
expect(cloned[0].data).toEqual({ source: 'signup' });
expect(cloned[0].data).not.toBe(routes[0].data);
expect(cloned[0]._loadedComponent).toBeUndefined();
expect(cloned[0]._loadedInjector).toBeUndefined();
expect(cloned[0]._loadedRoutes).toBeUndefined();
expect(cloned[0]._injector).toBeUndefined();
expect(cloned[0]._loadedNgModuleFactory).toBeUndefined();
expect(cloned[0]['ɵrouterPageId']).toBeUndefined();
expect(cloned[0].children).toEqual([
{
path: 'child',
loadChildren: routes[0].children[0].loadChildren,
},
]);
expect(cloned[0].children).not.toBe(routes[0].children);
expect(cloned[0].children[0]).not.toBe(routes[0].children[0]);
});

it('returns an empty array when routes are missing', () => {
expect(cloneRoutesForBootstrap(undefined)).toEqual([]);
expect(cloneRoutesForBootstrap(null)).toEqual([]);
});
});
Loading
Loading