@reforgium/presentia 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [2.1.2]: 14.04.2026
2
+ ### Fix:
3
+ - fixed `presentia-gen-namespaces` skipping spread (`...someRoutes`) and bare-reference (`someRoutes`) items in route arrays — generator now resolves them via imports and recurses into their exports
4
+ - fixed `presentia-gen-namespaces` not handling single-object route exports (`export const route: AppRoute = { ... }`) — added object-literal fallback in `parseRoutesFromExport` alongside the existing array-literal path
5
+ - fixed `LangPipe` NG0100 in Angular dev mode: cache-hit path was returning `existing.value()` directly, bypassing the namespace-loaded guard that cache-miss applied — both paths now go through a shared `applyNamespaceGuard` method
6
+
7
+ ---
8
+
1
9
  ## [2.1.1]: 07.04.2026
2
10
  ### Fix:
3
11
  - fixed `presentia-gen-namespaces` tsconfig loading to parse commented `tsconfig` files instead of failing on raw `JSON.parse(...)`
@@ -863,6 +863,40 @@ function parseRoutes(arrayLiteral, context) {
863
863
  }
864
864
  }
865
865
 
866
+ // Handle spread (...someRoutes) and bare references (someRoutes) that are not object literals
867
+ for (const item of splitTopLevelItems(arrayLiteral)) {
868
+ const trimmed = item.trim();
869
+
870
+ if (trimmed.startsWith('{')) continue;
871
+
872
+ const spreadMatch = /^\.\.\.\s*(\w+)$/.exec(trimmed);
873
+ const refName = spreadMatch ? spreadMatch[1] : /^\w+$/.test(trimmed) ? trimmed : null;
874
+
875
+ if (!refName) continue;
876
+
877
+ const imported = context.imports.get(refName);
878
+
879
+ if (!imported) continue;
880
+
881
+ const resolved = context.resolver(imported.specifier, context.routesFile);
882
+
883
+ if (!resolved) {
884
+ generatorReport?.addIssue({
885
+ type: 'unresolved-spread-routes',
886
+ filePath: context.routesFile,
887
+ message: `Could not resolve spread routes import "${imported.specifier}" (${refName})`,
888
+ });
889
+ continue;
890
+ }
891
+
892
+ routes.push(
893
+ ...parseRoutesFromExport(resolved, imported.imported, {
894
+ ...context,
895
+ routesFile: resolved,
896
+ }),
897
+ );
898
+ }
899
+
866
900
  return routes;
867
901
  }
868
902
 
@@ -876,20 +910,36 @@ function parseRoutesFromExport(filePath, exportName, context) {
876
910
  visitedRouteExports.add(visitKey);
877
911
 
878
912
  const source = stripComments(readText(filePath));
913
+ const newContext = {
914
+ ...context,
915
+ constObjects: parseConstObjects(filePath, context.resolver),
916
+ imports: parseImports(source),
917
+ routesFile: filePath,
918
+ };
919
+
879
920
  const arrayLiteral =
880
921
  extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*:\\s*[^=]+=\\s*\\[`, 'g')) ??
881
922
  extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*=\\s*\\[`, 'g'));
882
923
 
883
- if (!arrayLiteral) {
884
- return [];
924
+ if (arrayLiteral) {
925
+ return parseRoutes(arrayLiteral, newContext);
885
926
  }
886
927
 
887
- return parseRoutes(arrayLiteral, {
888
- ...context,
889
- constObjects: parseConstObjects(filePath, context.resolver),
890
- imports: parseImports(source),
891
- routesFile: filePath,
892
- });
928
+ // Fallback: try as a single route object export (e.g. export const analyticsRoutes: AppRoute = { ... })
929
+ const objectMatch =
930
+ new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*:\\s*[^=]+=\\s*\\{`, 'g').exec(source) ??
931
+ new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*=\\s*\\{`, 'g').exec(source);
932
+
933
+ if (objectMatch) {
934
+ const start = source.indexOf('{', objectMatch.index);
935
+ const end = findMatching(source, start, '{', '}');
936
+
937
+ if (start >= 0 && end >= 0) {
938
+ return parseRoutes(`[${source.slice(start, end + 1)}]`, newContext);
939
+ }
940
+ }
941
+
942
+ return [];
893
943
  }
894
944
 
895
945
  function collectComponentNamespaces(filePath, exportName, context) {
@@ -73,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
73
73
  */
74
74
  class AdaptiveService {
75
75
  /** @internal Signal of the current device type. */
76
- #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
76
+ #device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : /* istanbul ignore next */ []));
77
77
  /** @internal Signals of the current window width and height. */
78
- #width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
79
- #height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : []));
78
+ #width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : /* istanbul ignore next */ []));
79
+ #height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : /* istanbul ignore next */ []));
80
80
  /**
81
81
  * Current device type (reactive signal).
82
82
  * Possible values: `'desktop' | 'tablet' | 'mobile'`.
@@ -99,15 +99,15 @@ class AdaptiveService {
99
99
  * Computed signal indicating whether the current device is a desktop.
100
100
  * Used for conditional rendering or layout configuration.
101
101
  */
102
- isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : []));
103
- isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : []));
104
- isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : []));
105
- isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] : []));
102
+ isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : /* istanbul ignore next */ []));
103
+ isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
104
+ isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
105
+ isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] : /* istanbul ignore next */ []));
106
106
  /**
107
107
  * Computed signal determining whether the screen is in portrait orientation.
108
108
  * Returns `true` if window height is greater than width.
109
109
  */
110
- isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
110
+ isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : /* istanbul ignore next */ []));
111
111
  deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
112
112
  devicePriority = Object.keys(this.deviceBreakpoints);
113
113
  destroyRef = inject(DestroyRef);
@@ -200,10 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
200
200
  * the template is automatically added or removed from the DOM.
201
201
  */
202
202
  class IfDeviceDirective {
203
- deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
204
- atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] : []));
205
- betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] : []));
206
- inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : []));
203
+ deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : /* istanbul ignore next */ []));
204
+ atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] : /* istanbul ignore next */ []));
205
+ betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] : /* istanbul ignore next */ []));
206
+ inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : /* istanbul ignore next */ []));
207
207
  tpl = inject(TemplateRef);
208
208
  vcr = inject(ViewContainerRef);
209
209
  adaptive = inject(AdaptiveService);
@@ -435,14 +435,14 @@ function extractRouteParamKeys(path) {
435
435
  class RouteWatcher {
436
436
  router = inject(Router);
437
437
  destroyRef = inject(DestroyRef);
438
- #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : []));
439
- #deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : []));
440
- #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
441
- #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
442
- #mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : []));
443
- #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
444
- #routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : []));
445
- #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
438
+ #params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
439
+ #deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
440
+ #query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
441
+ #data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
442
+ #mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
443
+ #url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
444
+ #routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
445
+ #fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
446
446
  /** Params merged from root to deepest route. */
447
447
  params = this.#params.asReadonly();
448
448
  /** Params declared on the deepest route only. */
@@ -469,7 +469,7 @@ class RouteWatcher {
469
469
  url: this.#url(),
470
470
  routePattern: this.#routePattern(),
471
471
  fragment: this.#fragment(),
472
- }), ...(ngDevMode ? [{ debugName: "state" }] : []));
472
+ }), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
473
473
  constructor() {
474
474
  const read = () => {
475
475
  const snapshot = this.deepestSnapshot();
@@ -725,8 +725,8 @@ class LangService {
725
725
  ...LangService.BUILTIN_LANGS,
726
726
  ...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
727
727
  ]);
728
- #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
729
- #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
728
+ #lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
729
+ #cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
730
730
  #loadedNamespaces = new Set();
731
731
  #pendingLoads = new Map();
732
732
  #pendingBatchLoads = new Map();
@@ -743,7 +743,7 @@ class LangService {
743
743
  currentLang = computed(() => {
744
744
  const lang = this.#lang();
745
745
  return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
746
- }, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
746
+ }, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
747
747
  /**
748
748
  * Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
749
749
  * Expected that property `#lang` has `asReadonly` method that returns immutable representation.
@@ -1186,19 +1186,19 @@ class LangDirective {
1186
1186
  * Localization mode: defines which parts of the element will be translated.
1187
1187
  * @default 'all'
1188
1188
  */
1189
- lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
1189
+ lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
1190
1190
  /**
1191
1191
  * Explicit key for text content translation.
1192
1192
  */
1193
- reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
1193
+ reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
1194
1194
  /**
1195
1195
  * Explicit attribute-to-key map for translation.
1196
1196
  */
1197
- reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
1197
+ reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
1198
1198
  /**
1199
1199
  * Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
1200
1200
  */
1201
- langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
1201
+ langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
1202
1202
  el = inject(ElementRef);
1203
1203
  renderer = inject(Renderer2);
1204
1204
  service = inject(LangService);
@@ -1452,7 +1452,7 @@ class LangPipe {
1452
1452
  const existing = this.cache.get(key);
1453
1453
  if (existing) {
1454
1454
  if (now - existing.ts < this.ttlMs) {
1455
- return existing.value();
1455
+ return this.applyNamespaceGuard(query, existing.value());
1456
1456
  }
1457
1457
  this.cache.delete(key);
1458
1458
  }
@@ -1460,7 +1460,9 @@ class LangPipe {
1460
1460
  this.cache.set(key, { value: this.lang.observe(query, params ?? undefined), ts: now });
1461
1461
  this.evictIfNeeded();
1462
1462
  }
1463
- const value = this.cache.get(key).value();
1463
+ return this.applyNamespaceGuard(query, this.cache.get(key).value());
1464
+ }
1465
+ applyNamespaceGuard(query, value) {
1464
1466
  const ns = query.split('.', 1)[0];
1465
1467
  if (ns && !this.lang.isNamespaceLoaded(ns)) {
1466
1468
  const placeholder = this.config.placeholder;
@@ -1608,7 +1610,7 @@ class ThemeService {
1608
1610
  persistence = inject(THEME_PERSISTENCE_ADAPTER);
1609
1611
  isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1610
1612
  document = inject(DOCUMENT);
1611
- #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
1613
+ #theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
1612
1614
  /**
1613
1615
  * Current active theme (`light` or `dark`).
1614
1616
  *
@@ -1618,13 +1620,13 @@ class ThemeService {
1618
1620
  * <div [class]="themeService.theme()"></div>
1619
1621
  * ```
1620
1622
  */
1621
- theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
1623
+ theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
1622
1624
  /**
1623
1625
  * Convenient flag returning `true` if the light theme is active.
1624
1626
  * Suitable for conditional style application or resource selection.
1625
1627
  */
1626
- isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
1627
- isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : []));
1628
+ isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
1629
+ isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
1628
1630
  constructor() {
1629
1631
  effect(() => {
1630
1632
  if (!this.isBrowser) {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.1",
2
+ "version": "2.1.2",
3
3
  "name": "@reforgium/presentia",
4
4
  "description": "Angular infrastructure library for i18n, route-aware namespace preload, theming, adaptive breakpoints, route state, and SEO",
5
5
  "author": "rtommievich",
@@ -608,6 +608,7 @@ declare class LangPipe implements PipeTransform {
608
608
  private readonly maxCacheSize;
609
609
  constructor();
610
610
  transform(query: string | null | undefined, params?: LangParams | null): string;
611
+ private applyNamespaceGuard;
611
612
  private makeKey;
612
613
  private evictIfNeeded;
613
614
  private warnUnresolvedKey;