Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
255 changes: 220 additions & 35 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
27 changes: 27 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,27 @@
import { resetAngularHmrCompiledComponents } 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);
});
});
19 changes: 19 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,19 @@
type AngularCoreWithCompiledComponentReset = {
ɵresetCompiledComponents?: () => void;
};

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;
}
}
61 changes: 61 additions & 0 deletions packages/angular/src/lib/legacy/router/hmr-route-cache-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
type AngularHmrRouteLike = {
children?: AngularHmrRouteLike[];
_injector?: unknown;
_loadedComponent?: unknown;
_loadedInjector?: unknown;
_loadedRoutes?: AngularHmrRouteLike[];
};

const ROUTE_CACHE_KEYS = ['_loadedComponent', '_loadedInjector', '_loadedRoutes', '_injector'] as const;

function clearRouteCacheField(route: Record<string, unknown>, key: (typeof ROUTE_CACHE_KEYS)[number]): boolean {
if (!Object.prototype.hasOwnProperty.call(route, key) && route[key] === undefined) {
return false;
}

try {
delete route[key];
} catch {
try {
route[key] = undefined;
} catch {}
}

return true;
}

export function clearAngularHmrRouteConfigCaches(routes: AngularHmrRouteLike[] | undefined | null): number {
const seen = new Set<AngularHmrRouteLike>();
let cleared = 0;

const visitRoute = (route: AngularHmrRouteLike | undefined | null): void => {
if (!route || seen.has(route)) {
return;
}

seen.add(route);

const childRoutes = Array.isArray(route.children) ? route.children : [];
const loadedRoutes = Array.isArray(route._loadedRoutes) ? route._loadedRoutes : [];

for (const childRoute of childRoutes) {
visitRoute(childRoute);
}

for (const loadedRoute of loadedRoutes) {
visitRoute(loadedRoute);
}

for (const key of ROUTE_CACHE_KEYS) {
if (clearRouteCacheField(route as Record<string, unknown>, key)) {
cleared += 1;
}
}
};

for (const route of Array.isArray(routes) ? routes : []) {
visitRoute(route);
}

return cleared;
}
69 changes: 69 additions & 0 deletions packages/angular/src/lib/legacy/router/hmr-route-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { clearAngularHmrRouteConfigCaches } from './hmr-route-cache-core';

describe('Angular HMR route cache clearing', () => {
it('clears lazy route caches recursively while preserving public route fields', () => {
const grandchild = {
path: 'details',
_loadedComponent: { name: 'DetailsComponent' },
_loadedInjector: { token: 'details' },
};
const child = {
path: 'survey',
children: [grandchild],
_loadedComponent: { name: 'SurveyComponent' },
_loadedInjector: { token: 'survey' },
_injector: { token: 'child-injector' },
};
const route = {
path: 'onboarding-flow',
children: [child],
_loadedRoutes: [
{
path: 'lazy',
_loadedComponent: { name: 'LazyComponent' },
_loadedRoutes: [
{
path: 'nested',
_injector: { token: 'nested-injector' },
},
],
},
],
};

const cleared = clearAngularHmrRouteConfigCaches([route]);

expect(cleared).toBe(9);
expect(route.path).toBe('onboarding-flow');
expect(child.path).toBe('survey');
expect(grandchild.path).toBe('details');
expect((route as any)._loadedRoutes).toBeUndefined();
expect((child as any)._loadedComponent).toBeUndefined();
expect((child as any)._loadedInjector).toBeUndefined();
expect((child as any)._injector).toBeUndefined();
expect((grandchild as any)._loadedComponent).toBeUndefined();
expect((grandchild as any)._loadedInjector).toBeUndefined();
});

it('does not loop forever when route graphs reuse the same child object', () => {
const shared = {
path: 'shared',
_loadedComponent: { name: 'SharedComponent' },
};
const routes = [
{
path: 'a',
children: [shared],
},
{
path: 'b',
_loadedRoutes: [shared],
},
];

const cleared = clearAngularHmrRouteConfigCaches(routes as any);

expect(cleared).toBe(2);
expect((shared as any)._loadedComponent).toBeUndefined();
});
});
Loading
Loading