@ng-prism/core 22.3.0 → 22.4.1

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.
Files changed (166) hide show
  1. package/README.md +13 -13
  2. package/dist/app/canvas/canvas-bg.styles.d.ts +7 -12
  3. package/dist/app/canvas/canvas-bg.styles.d.ts.map +1 -1
  4. package/dist/app/canvas/canvas-bg.styles.js +35 -37
  5. package/dist/app/canvas/measure-geometry.d.ts +140 -0
  6. package/dist/app/canvas/measure-geometry.d.ts.map +1 -0
  7. package/dist/app/canvas/measure-geometry.js +217 -0
  8. package/dist/app/canvas/measure-keyboard.d.ts +20 -0
  9. package/dist/app/canvas/measure-keyboard.d.ts.map +1 -0
  10. package/dist/app/canvas/measure-keyboard.js +34 -0
  11. package/dist/app/canvas/measure-quad.d.ts +114 -0
  12. package/dist/app/canvas/measure-quad.d.ts.map +1 -0
  13. package/dist/app/canvas/measure-quad.js +173 -0
  14. package/dist/app/canvas/measure-snap.d.ts +35 -0
  15. package/dist/app/canvas/measure-snap.d.ts.map +1 -0
  16. package/dist/app/canvas/measure-snap.js +63 -0
  17. package/dist/app/canvas/prism-canvas-measure.component.d.ts +142 -0
  18. package/dist/app/canvas/prism-canvas-measure.component.d.ts.map +1 -0
  19. package/dist/app/canvas/prism-canvas-measure.component.js +554 -0
  20. package/dist/app/canvas/prism-canvas-pin-row.component.d.ts +23 -0
  21. package/dist/app/canvas/prism-canvas-pin-row.component.d.ts.map +1 -0
  22. package/dist/app/canvas/prism-canvas-pin-row.component.js +64 -0
  23. package/dist/app/canvas/prism-canvas-toolbar.component.d.ts +22 -39
  24. package/dist/app/canvas/prism-canvas-toolbar.component.d.ts.map +1 -1
  25. package/dist/app/canvas/prism-canvas-toolbar.component.js +61 -69
  26. package/dist/app/canvas/prism-template-popover.component.js +1 -1
  27. package/dist/app/canvas/viewport-snap.d.ts +1 -1
  28. package/dist/app/canvas/viewport-snap.js +1 -1
  29. package/dist/app/component-head/head-metrics.d.ts +2 -2
  30. package/dist/app/component-head/head-metrics.js +1 -1
  31. package/dist/app/component-head/prism-component-head.component.d.ts +2 -2
  32. package/dist/app/component-head/prism-component-head.component.d.ts.map +1 -1
  33. package/dist/app/component-head/prism-component-head.component.js +11 -18
  34. package/dist/app/component-head/prism-head-gauge.component.js +1 -1
  35. package/dist/app/component-head/prism-head-info.component.d.ts +1 -1
  36. package/dist/app/component-head/prism-head-info.component.js +2 -2
  37. package/dist/app/component-head/prism-stat.component.d.ts +1 -1
  38. package/dist/app/component-head/prism-stat.component.js +2 -2
  39. package/dist/app/directives/prism-resizer.directive.d.ts +1 -1
  40. package/dist/app/directives/prism-resizer.directive.js +2 -2
  41. package/dist/app/directives/resize-value.d.ts +2 -2
  42. package/dist/app/directives/resize-value.js +2 -2
  43. package/dist/app/discovery-manifest.d.ts +2 -2
  44. package/dist/app/discovery-manifest.d.ts.map +1 -1
  45. package/dist/app/discovery-manifest.js +11 -15
  46. package/dist/app/header/prism-header.component.js +1 -1
  47. package/dist/app/icons/icon-registry.d.ts +2 -2
  48. package/dist/app/icons/icon-registry.d.ts.map +1 -1
  49. package/dist/app/icons/icon-registry.js +3 -2
  50. package/dist/app/icons/prism-icon.component.d.ts +2 -2
  51. package/dist/app/icons/prism-icon.component.js +4 -4
  52. package/dist/app/index.d.ts.map +1 -1
  53. package/dist/app/index.js +14 -21
  54. package/dist/app/overview/overview-visibility.d.ts +1 -1
  55. package/dist/app/overview/overview-visibility.js +1 -1
  56. package/dist/app/overview/prism-overview-cell.component.d.ts +2 -2
  57. package/dist/app/overview/prism-overview-cell.component.d.ts.map +1 -1
  58. package/dist/app/overview/prism-overview-cell.component.js +9 -10
  59. package/dist/app/overview/prism-overview.component.d.ts +2 -2
  60. package/dist/app/overview/prism-overview.component.js +2 -2
  61. package/dist/app/page-renderer/prism-page-renderer.component.js +1 -1
  62. package/dist/app/panels/a11y/a11y-audit.service.js +2 -2
  63. package/dist/app/panels/a11y/a11y-summary.d.ts +3 -4
  64. package/dist/app/panels/a11y/a11y-summary.d.ts.map +1 -1
  65. package/dist/app/panels/a11y/a11y-summary.js +3 -4
  66. package/dist/app/panels/a11y/a11y-thresholds.d.ts +1 -1
  67. package/dist/app/panels/a11y/a11y-thresholds.d.ts.map +1 -1
  68. package/dist/app/panels/a11y/a11y-thresholds.js +1 -1
  69. package/dist/app/panels/a11y/a11y.types.d.ts +1 -1
  70. package/dist/app/panels/a11y/a11y.types.d.ts.map +1 -1
  71. package/dist/app/panels/builtin-navigation-decorations.d.ts +1 -1
  72. package/dist/app/panels/builtin-navigation-decorations.js +1 -1
  73. package/dist/app/panels/controls/non-editable-input-types.js +1 -1
  74. package/dist/app/panels/panel-host/panel-badge.d.ts +1 -1
  75. package/dist/app/provide-prism.js +1 -1
  76. package/dist/app/renderer/known-inputs.d.ts +1 -1
  77. package/dist/app/renderer/known-inputs.js +1 -1
  78. package/dist/app/renderer/overlay-resolver.d.ts +17 -3
  79. package/dist/app/renderer/overlay-resolver.d.ts.map +1 -1
  80. package/dist/app/renderer/overlay-resolver.js +9 -4
  81. package/dist/app/renderer/prism-renderer.component.d.ts +3 -3
  82. package/dist/app/renderer/prism-renderer.component.d.ts.map +1 -1
  83. package/dist/app/renderer/prism-renderer.component.js +25 -16
  84. package/dist/app/renderer/projectable-content.js +3 -3
  85. package/dist/app/services/prism-canvas.service.d.ts +12 -1
  86. package/dist/app/services/prism-canvas.service.d.ts.map +1 -1
  87. package/dist/app/services/prism-canvas.service.js +23 -5
  88. package/dist/app/services/prism-capture.service.d.ts +24 -50
  89. package/dist/app/services/prism-capture.service.d.ts.map +1 -1
  90. package/dist/app/services/prism-capture.service.js +60 -140
  91. package/dist/app/services/prism-measure.service.d.ts +44 -0
  92. package/dist/app/services/prism-measure.service.d.ts.map +1 -0
  93. package/dist/app/services/prism-measure.service.js +91 -0
  94. package/dist/app/services/prism-panel.service.d.ts +2 -2
  95. package/dist/app/services/prism-panel.service.js +2 -2
  96. package/dist/app/services/prism-persistence.service.js +1 -1
  97. package/dist/app/services/prism-renderer.service.js +1 -1
  98. package/dist/app/services/prism-url-state.service.js +1 -1
  99. package/dist/app/services/prism-variant-bg.service.d.ts +10 -24
  100. package/dist/app/services/prism-variant-bg.service.d.ts.map +1 -1
  101. package/dist/app/services/prism-variant-bg.service.js +10 -24
  102. package/dist/app/shared/prism-metric-badge.component.d.ts +9 -15
  103. package/dist/app/shared/prism-metric-badge.component.d.ts.map +1 -1
  104. package/dist/app/shared/prism-metric-badge.component.js +10 -16
  105. package/dist/app/shell/prism-shell.component.d.ts.map +1 -1
  106. package/dist/app/shell/prism-shell.component.js +12 -9
  107. package/dist/app/sidebar/navigation-decorations.d.ts +4 -4
  108. package/dist/app/sidebar/navigation-decorations.js +2 -2
  109. package/dist/app/sidebar/prism-sidebar.component.d.ts +2 -2
  110. package/dist/app/sidebar/prism-sidebar.component.js +3 -3
  111. package/dist/app/theme/prism-default-theme.js +3 -3
  112. package/dist/app/variant-ribbon/spectrum.d.ts +1 -1
  113. package/dist/app/variant-ribbon/spectrum.js +1 -1
  114. package/dist/app/view-tab-bar/next-view-id.d.ts +1 -1
  115. package/dist/app/view-tab-bar/next-view-id.js +1 -1
  116. package/dist/builder/a11y/a11y-report-reader.d.ts +1 -1
  117. package/dist/builder/a11y/a11y-report-reader.js +1 -1
  118. package/dist/builder/manifest/runtime-manifest.generator.js +6 -2
  119. package/dist/builder/plugin-runner/plugin-runner.js +1 -1
  120. package/dist/builder/scanner/ast-utils.d.ts +46 -2
  121. package/dist/builder/scanner/ast-utils.d.ts.map +1 -1
  122. package/dist/builder/scanner/ast-utils.js +337 -31
  123. package/dist/builder/scanner/class-hierarchy.d.ts +33 -0
  124. package/dist/builder/scanner/class-hierarchy.d.ts.map +1 -0
  125. package/dist/builder/scanner/class-hierarchy.js +87 -0
  126. package/dist/builder/scanner/component.scanner.d.ts +7 -1
  127. package/dist/builder/scanner/component.scanner.d.ts.map +1 -1
  128. package/dist/builder/scanner/component.scanner.js +106 -58
  129. package/dist/builder/scanner/input.extractor.d.ts +7 -4
  130. package/dist/builder/scanner/input.extractor.d.ts.map +1 -1
  131. package/dist/builder/scanner/input.extractor.js +153 -55
  132. package/dist/builder/scanner/manifest.generator.js +6 -2
  133. package/dist/builder/scanner/scanner.d.ts +3 -1
  134. package/dist/builder/scanner/scanner.d.ts.map +1 -1
  135. package/dist/builder/scanner/scanner.js +6 -4
  136. package/dist/builder/scanner/showcase-diagnostics.d.ts +25 -0
  137. package/dist/builder/scanner/showcase-diagnostics.d.ts.map +1 -0
  138. package/dist/builder/scanner/showcase-diagnostics.js +96 -0
  139. package/dist/builder/shared/prism-pipeline.d.ts +1 -1
  140. package/dist/builder/shared/prism-pipeline.d.ts.map +1 -1
  141. package/dist/builder/shared/prism-pipeline.js +12 -14
  142. package/dist/builder/watcher/prism-watcher.js +2 -2
  143. package/dist/decorator/showcase.types.d.ts +11 -9
  144. package/dist/decorator/showcase.types.d.ts.map +1 -1
  145. package/dist/index.js +3 -3
  146. package/dist/plugin/define-config.js +1 -1
  147. package/dist/plugin/index.d.ts.map +1 -1
  148. package/dist/plugin/index.js +5 -9
  149. package/dist/plugin/plugin.types.d.ts +30 -13
  150. package/dist/plugin/plugin.types.d.ts.map +1 -1
  151. package/dist/schematics/migrations/v22-0-cache-dir-rename/index.js +2 -2
  152. package/dist/schematics/ng-add/index.js +4 -4
  153. package/dist/shared/canvas-bg.type.d.ts +5 -6
  154. package/dist/shared/canvas-bg.type.d.ts.map +1 -1
  155. package/dist/shared/canvas-bg.type.js +4 -5
  156. package/dist/shared/measure.type.d.ts +52 -0
  157. package/dist/shared/measure.type.d.ts.map +1 -0
  158. package/dist/shared/measure.type.js +21 -0
  159. package/dist/shared/variant-bg.d.ts +20 -31
  160. package/dist/shared/variant-bg.d.ts.map +1 -1
  161. package/dist/shared/variant-bg.js +20 -31
  162. package/dist/shared/viewport.type.d.ts +15 -19
  163. package/dist/shared/viewport.type.d.ts.map +1 -1
  164. package/dist/shared/viewport.type.js +15 -19
  165. package/dist/transformer/showcase-strip.transformer.js +1 -1
  166. package/package.json +5 -5
@@ -1,8 +1,10 @@
1
1
  import ts from 'typescript';
2
2
  import { CANVAS_BGS } from '../../shared/canvas-bg.type.js';
3
3
  import { CANVAS_LAYOUTS } from '../../shared/canvas-layout.type.js';
4
- import { evaluateExpression, findDecorator, getDecoratorArgument } from './ast-utils.js';
4
+ import { evaluateExpression, evaluateStatic, findDecorator, getDecoratorArgument, UNEVALUABLE } from './ast-utils.js';
5
+ import { getClassHierarchy } from './class-hierarchy.js';
5
6
  import { extractInputs, extractOutputs } from './input.extractor.js';
7
+ import { describeDeprecatedProviders, describeUnevaluable, describeUnreadableBase } from './showcase-diagnostics.js';
6
8
  const COMPONENT_STATUSES = ['stable', 'beta', 'wip', 'deprecated'];
7
9
  function isComponentStatus(value) {
8
10
  return typeof value === 'string' && COMPONENT_STATUSES.includes(value);
@@ -11,14 +13,14 @@ function isComponentStatus(value) {
11
13
  * Backgrounds that still work but should not be reached for any more.
12
14
  *
13
15
  * `checker` draws the same checkerboard as `transparent` while browsing, so
14
- * the canvas cannot tell them apart — but it captures as `--prism-bg-surface`,
16
+ * the canvas cannot tell them apart. It captures as `--prism-bg-surface`, though,
15
17
  * a *theme* token, which makes a baseline recorded on it depend on the theme
16
18
  * the runner's browser started in. Since `transparent` took over as the
17
19
  * default, it is the value that looks like transparency and is not.
18
20
  *
19
- * Warned rather than rejected: the value is valid, and dropping it would
20
- * change what a component renders on — a break wearing a warning's clothes.
21
- * Scheduled for removal in 23.0.0, the next Angular-aligned major.
21
+ * Only warned about: the value is valid, and rejecting it would change what a
22
+ * component renders on, which is a breaking change. Scheduled for removal in
23
+ * 23.0.0, the next Angular-aligned major.
22
24
  */
23
25
  const DEPRECATED_BGS = {
24
26
  checker: "use 'transparent' for the same look with a capture that keeps its alpha, or 'light'/'dark' for an absolute colour"
@@ -27,7 +29,7 @@ function warnDeprecatedBg(bg, where) {
27
29
  const advice = DEPRECATED_BGS[bg];
28
30
  if (!advice)
29
31
  return;
30
- console.warn(`⚠ ng-prism: ${where} declares bg "${bg}", which is deprecated and will ` + `be removed in 23.0.0 — ${advice}.`);
32
+ console.warn(`⚠ ng-prism: ${where} declares bg "${bg}", which is deprecated and will ` + `be removed in 23.0.0; ${advice}.`);
31
33
  }
32
34
  function isCanvasBg(value) {
33
35
  return typeof value === 'string' && CANVAS_BGS.includes(value);
@@ -37,15 +39,33 @@ function isCanvasLayout(value) {
37
39
  }
38
40
  /**
39
41
  * Scan exported symbols for Angular components annotated with @Showcase.
42
+ *
43
+ * Every @Showcase value the scan has to drop is printed and collected in
44
+ * `diagnostics`, once: sharing the array across entry points keeps a
45
+ * component exported from several of them from repeating its warnings.
46
+ * `notices` does the same for warnings about what a component inherits,
47
+ * which are not diagnostics.
40
48
  */
41
- export function scanComponents(exports, checker) {
49
+ export function scanComponents(exports, checker, diagnostics = [], notices = new Set()) {
42
50
  const components = [];
51
+ const report = (message) => {
52
+ if (diagnostics.includes(message))
53
+ return;
54
+ diagnostics.push(message);
55
+ console.warn(`⚠ ng-prism: ${message}`);
56
+ };
57
+ const notify = (message) => {
58
+ if (notices.has(message))
59
+ return;
60
+ notices.add(message);
61
+ console.warn(`⚠ ng-prism: ${message}`);
62
+ };
43
63
  for (const sym of exports) {
44
64
  const resolved = sym.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym;
45
65
  const classDecl = resolved.declarations?.find(ts.isClassDeclaration);
46
66
  if (!classDecl)
47
67
  continue;
48
- // Perf: cheap pre-filter — skip the full decorator walk for files that don't
68
+ // Perf: cheap pre-filter that skips the full decorator walk for files that don't
49
69
  // mention Showcase at all. False positives (string-literal containing "@Showcase")
50
70
  // just lose the optimization for that file; correctness is unaffected.
51
71
  if (!classDecl.getSourceFile().text.includes('@Showcase'))
@@ -54,15 +74,23 @@ export function scanComponents(exports, checker) {
54
74
  if (!showcaseDecorator)
55
75
  continue;
56
76
  const className = classDecl.name?.text ?? 'Anonymous';
57
- const showcaseConfig = extractShowcaseConfig(showcaseDecorator, className);
77
+ const showcaseConfig = extractShowcaseConfig(showcaseDecorator, className, report, checker);
58
78
  if (!showcaseConfig)
59
79
  continue;
60
- const componentMeta = extractComponentMeta(classDecl);
61
- const inputs = extractInputs(classDecl, checker);
62
- const outputs = extractOutputs(classDecl, checker);
80
+ const componentMeta = extractComponentMeta(classDecl, checker);
81
+ const hierarchy = getClassHierarchy(classDecl, checker);
82
+ const inputs = extractInputs(classDecl, checker, hierarchy);
83
+ const outputs = extractOutputs(classDecl, checker, hierarchy);
63
84
  const filePath = classDecl.getSourceFile().fileName;
64
- if (hasDecoratorInputs(classDecl)) {
65
- console.warn(`⚠ ng-prism: ${className} uses @Input() decorators which are not fully supported. ` + `Migrate to input() signals for full ng-prism support.`);
85
+ const decoratorInputs = hierarchy.classes.find((c) => c.definesMetadata && hasDecoratorInputs(c.classDecl))?.classDecl;
86
+ if (decoratorInputs) {
87
+ const where = decoratorInputs === classDecl ? 'uses @Input() decorators' : `inherits @Input() decorators from ${decoratorInputs.name?.text ?? 'an anonymous class'},`;
88
+ notify(`${className} ${where} which are not fully supported. Migrate to input() signals for full ng-prism support.`);
89
+ }
90
+ // Not diagnostics: strictShowcase would fail the build over a base
91
+ // class in a package, which the component's author cannot change.
92
+ for (const base of hierarchy.unreadable) {
93
+ notify(describeUnreadableBase(className, base));
66
94
  }
67
95
  components.push({
68
96
  className,
@@ -70,7 +98,11 @@ export function scanComponents(exports, checker) {
70
98
  showcaseConfig,
71
99
  inputs,
72
100
  outputs,
73
- componentMeta
101
+ componentMeta,
102
+ // Plugins look a base class up by name, so an anonymous one is of no use to them.
103
+ baseClasses: hierarchy.classes
104
+ .slice(1)
105
+ .flatMap(({ classDecl: base }) => (base.name ? [{ className: base.name.text, filePath: base.getSourceFile().fileName }] : []))
74
106
  });
75
107
  }
76
108
  return components;
@@ -84,15 +116,29 @@ function hasDecoratorInputs(classDecl) {
84
116
  }
85
117
  return false;
86
118
  }
87
- function extractShowcaseConfig(decorator, className) {
119
+ function extractShowcaseConfig(decorator, className, report, checker) {
88
120
  const arg = getDecoratorArgument(decorator);
89
121
  if (!arg)
90
122
  return undefined;
91
- const raw = evaluateExpression(arg);
92
- if (!raw || typeof raw !== 'object')
123
+ // Reported once for the whole field below, wherever it came from: the literal, a spread or a constant.
124
+ let providers;
125
+ const raw = evaluateStatic(arg, {
126
+ checker,
127
+ report: (issue) => {
128
+ if (issue.path[0] === 'providers') {
129
+ providers ??= issue.node;
130
+ return;
131
+ }
132
+ report(describeUnevaluable(className, arg, issue));
133
+ }
134
+ });
135
+ if (providers || (typeof raw === 'object' && raw !== null && 'providers' in raw)) {
136
+ report(describeDeprecatedProviders(className, arg, providers));
137
+ }
138
+ if (raw === UNEVALUABLE || !raw || typeof raw !== 'object')
93
139
  return undefined;
94
140
  if (!('title' in raw)) {
95
- console.warn(`⚠ ng-prism: ${className} has @Showcase without a "title" field — skipping. ` + `Add a title so it can appear in the styleguide.`);
141
+ report(`${className} has @Showcase without a "title" field, skipping. ` + `Add a title so it can appear in the styleguide.`);
96
142
  return undefined;
97
143
  }
98
144
  const obj = raw;
@@ -108,6 +154,12 @@ function extractShowcaseConfig(decorator, className) {
108
154
  if (typeof obj['sectionOrder'] === 'number') {
109
155
  config.sectionOrder = obj['sectionOrder'];
110
156
  }
157
+ if (typeof obj['categoryOrder'] === 'number') {
158
+ config.categoryOrder = obj['categoryOrder'];
159
+ }
160
+ if (typeof obj['componentOrder'] === 'number') {
161
+ config.componentOrder = obj['componentOrder'];
162
+ }
111
163
  if (obj['tags'])
112
164
  config.tags = obj['tags'];
113
165
  if (obj['meta'])
@@ -121,7 +173,7 @@ function extractShowcaseConfig(decorator, className) {
121
173
  config.status = obj['status'];
122
174
  }
123
175
  else {
124
- console.warn(`⚠ ng-prism: ${className} declares invalid status "${String(obj['status'])}" — ` + `expected one of: ${COMPONENT_STATUSES.join(', ')}. Skipping.`);
176
+ report(`${className} declares invalid status "${String(obj['status'])}", ` + `expected one of: ${COMPONENT_STATUSES.join(', ')}. Skipping.`);
125
177
  }
126
178
  }
127
179
  if (obj['bg'] !== undefined) {
@@ -130,7 +182,7 @@ function extractShowcaseConfig(decorator, className) {
130
182
  warnDeprecatedBg(obj['bg'], className);
131
183
  }
132
184
  else {
133
- console.warn(`⚠ ng-prism: ${className} declares invalid bg "${String(obj['bg'])}" — ` + `expected one of: ${CANVAS_BGS.join(', ')}. Skipping.`);
185
+ report(`${className} declares invalid bg "${String(obj['bg'])}", ` + `expected one of: ${CANVAS_BGS.join(', ')}. Skipping.`);
134
186
  }
135
187
  }
136
188
  if (obj['canvasLayout'] !== undefined) {
@@ -138,7 +190,7 @@ function extractShowcaseConfig(decorator, className) {
138
190
  config.canvasLayout = obj['canvasLayout'];
139
191
  }
140
192
  else {
141
- console.warn(`⚠ ng-prism: ${className} declares invalid canvasLayout "${String(obj['canvasLayout'])}" — ` + `expected one of: ${CANVAS_LAYOUTS.join(', ')}. Skipping.`);
193
+ report(`${className} declares invalid canvasLayout "${String(obj['canvasLayout'])}", ` + `expected one of: ${CANVAS_LAYOUTS.join(', ')}. Skipping.`);
142
194
  }
143
195
  }
144
196
  if (Array.isArray(obj['variants'])) {
@@ -148,14 +200,14 @@ function extractShowcaseConfig(decorator, className) {
148
200
  warnDeprecatedBg(variant['bg'], `${className} variant "${String(variant['name'])}"`);
149
201
  }
150
202
  if (variant['bg'] !== undefined && !isCanvasBg(variant['bg'])) {
151
- console.warn(`⚠ ng-prism: ${className} variant "${String(variant['name'])}" declares ` +
152
- `invalid bg "${String(variant['bg'])}" — expected one of: ` +
203
+ report(`${className} variant "${String(variant['name'])}" declares ` +
204
+ `invalid bg "${String(variant['bg'])}", expected one of: ` +
153
205
  `${CANVAS_BGS.join(', ')}. Skipping.`);
154
206
  delete cleaned['bg'];
155
207
  }
156
208
  if (variant['canvasLayout'] !== undefined && !isCanvasLayout(variant['canvasLayout'])) {
157
- console.warn(`⚠ ng-prism: ${className} variant "${String(variant['name'])}" declares ` +
158
- `invalid canvasLayout "${String(variant['canvasLayout'])}" — expected one of: ${CANVAS_LAYOUTS.join(', ')}. Skipping.`);
209
+ report(`${className} variant "${String(variant['name'])}" declares ` +
210
+ `invalid canvasLayout "${String(variant['canvasLayout'])}", expected one of: ${CANVAS_LAYOUTS.join(', ')}. Skipping.`);
159
211
  delete cleaned['canvasLayout'];
160
212
  }
161
213
  return cleaned;
@@ -163,38 +215,34 @@ function extractShowcaseConfig(decorator, className) {
163
215
  }
164
216
  return config;
165
217
  }
166
- function extractComponentMeta(classDecl) {
218
+ function extractComponentMeta(classDecl, checker) {
167
219
  const componentDecorator = findDecorator(classDecl, 'Component');
168
- if (componentDecorator) {
169
- const arg = getDecoratorArgument(componentDecorator);
170
- if (!arg)
171
- return { selector: '', standalone: true, isDirective: false };
172
- const raw = evaluateExpression(arg);
173
- if (!raw || typeof raw !== 'object') {
174
- return { selector: '', standalone: true, isDirective: false };
175
- }
176
- const obj = raw;
177
- return {
178
- selector: obj['selector'] ?? '',
179
- standalone: obj['standalone'] !== false,
180
- isDirective: false
181
- };
182
- }
183
- const directiveDecorator = findDecorator(classDecl, 'Directive');
184
- if (directiveDecorator) {
185
- const arg = getDecoratorArgument(directiveDecorator);
186
- if (!arg)
187
- return { selector: '', standalone: true, isDirective: true };
188
- const raw = evaluateExpression(arg);
189
- if (!raw || typeof raw !== 'object') {
190
- return { selector: '', standalone: true, isDirective: true };
191
- }
192
- const obj = raw;
193
- return {
194
- selector: obj['selector'] ?? '',
195
- standalone: obj['standalone'] !== false,
196
- isDirective: true
197
- };
220
+ const decorator = componentDecorator ?? findDecorator(classDecl, 'Directive');
221
+ const isDirective = !componentDecorator && decorator !== undefined;
222
+ const arg = decorator && getDecoratorArgument(decorator);
223
+ if (!arg)
224
+ return { selector: '', standalone: true, isDirective };
225
+ const selector = readDecoratorField(arg, 'selector', checker);
226
+ return {
227
+ selector: typeof selector === 'string' ? selector : '',
228
+ standalone: readDecoratorField(arg, 'standalone', checker) !== false,
229
+ isDirective
230
+ };
231
+ }
232
+ /**
233
+ * One field of `@Component`/`@Directive` metadata, read from its own
234
+ * property where the literal has one, so that `imports`, `providers` and the
235
+ * rest are not resolved only to be thrown away. A spread or a shorthand could
236
+ * supply the field from elsewhere, so then the whole argument is evaluated.
237
+ */
238
+ function readDecoratorField(arg, key, checker) {
239
+ if (ts.isObjectLiteralExpression(arg) && !arg.properties.some(ts.isSpreadAssignment)) {
240
+ const prop = arg.properties.find((p) => p.name !== undefined && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === key);
241
+ if (!prop)
242
+ return undefined;
243
+ if (ts.isPropertyAssignment(prop))
244
+ return evaluateExpression(prop.initializer, checker);
198
245
  }
199
- return { selector: '', standalone: true, isDirective: false };
246
+ const raw = evaluateExpression(arg, checker);
247
+ return typeof raw === 'object' && raw !== null ? raw[key] : undefined;
200
248
  }
@@ -1,11 +1,14 @@
1
1
  import ts from 'typescript';
2
2
  import type { InputMeta, OutputMeta } from '../../plugin/plugin.types.js';
3
3
  /**
4
- * Extract all @Input(), input() and model() signal metadata from a class declaration.
4
+ * Extract all @Input(), input() and model() signal metadata from a class
5
+ * declaration and the base classes Angular inherits inputs from. An input the
6
+ * subclass redeclares wins over the one in its base class.
5
7
  */
6
- export declare function extractInputs(classDecl: ts.ClassDeclaration, checker: ts.TypeChecker): InputMeta[];
8
+ export declare function extractInputs(classDecl: ts.ClassDeclaration, checker: ts.TypeChecker, hierarchy?: import("./class-hierarchy.js").ClassHierarchy): InputMeta[];
7
9
  /**
8
- * Extract all @Output() and output() signal metadata from a class declaration.
10
+ * Extract all @Output() and output() signal metadata from a class declaration
11
+ * and the base classes Angular inherits outputs from.
9
12
  */
10
- export declare function extractOutputs(classDecl: ts.ClassDeclaration, checker: ts.TypeChecker): OutputMeta[];
13
+ export declare function extractOutputs(classDecl: ts.ClassDeclaration, checker: ts.TypeChecker, hierarchy?: import("./class-hierarchy.js").ClassHierarchy): OutputMeta[];
11
14
  //# sourceMappingURL=input.extractor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input.extractor.d.ts","sourceRoot":"","sources":["../../../src/builder/scanner/input.extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG1E;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC,WAAW,GAAG,SAAS,EAAE,CAmDlG;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC,WAAW,GAAG,UAAU,EAAE,CA2BpG"}
1
+ {"version":3,"file":"input.extractor.d.ts","sourceRoot":"","sources":["../../../src/builder/scanner/input.extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAI1E;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC,WAAW,EAAE,SAAS,gDAAwC,GAAG,SAAS,EAAE,CAuCrJ;AAsED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC,WAAW,EAAE,SAAS,gDAAwC,GAAG,UAAU,EAAE,CAuBvJ"}
@@ -1,76 +1,174 @@
1
1
  import ts from 'typescript';
2
- import { evaluateExpression, findDecorator, getDecoratorArgument, getJsDocComment } from './ast-utils.js';
2
+ import { evaluateStatic, findDecorator, getDecoratorArgument, getJsDocComment } from './ast-utils.js';
3
+ import { getClassHierarchy } from './class-hierarchy.js';
3
4
  /**
4
- * Extract all @Input(), input() and model() signal metadata from a class declaration.
5
+ * Extract all @Input(), input() and model() signal metadata from a class
6
+ * declaration and the base classes Angular inherits inputs from. An input the
7
+ * subclass redeclares wins over the one in its base class.
5
8
  */
6
- export function extractInputs(classDecl, checker) {
9
+ export function extractInputs(classDecl, checker, hierarchy = getClassHierarchy(classDecl, checker)) {
7
10
  const inputs = [];
8
- for (const member of classDecl.members) {
9
- if (!ts.isPropertyDeclaration(member))
10
- continue;
11
- const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
12
- if (!name)
13
- continue;
14
- const inputDecorator = findDecorator(member, 'Input');
15
- if (inputDecorator) {
16
- const required = isDecoratorInputRequired(inputDecorator);
17
- const defaultValue = member.initializer ? evaluateExpression(member.initializer) : undefined;
18
- const doc = getJsDocComment(member, checker);
19
- const { type, values, rawType } = resolveDecoratorInputType(member, checker);
20
- inputs.push({
21
- name,
22
- type,
23
- rawType,
24
- required,
25
- ...(values && { values }),
26
- ...(defaultValue !== undefined && { defaultValue }),
27
- ...(doc && { doc })
28
- });
29
- continue;
30
- }
31
- const signalCall = getInputSignalCall(member);
32
- if (signalCall) {
33
- const required = isSignalInputRequired(signalCall);
34
- const defaultValue = !required && signalCall.arguments.length > 0 ? evaluateExpression(signalCall.arguments[0]) : undefined;
35
- const doc = getJsDocComment(member, checker);
36
- const { type, values, rawType } = resolveSignalInputType(signalCall, checker);
37
- inputs.push({
38
- name,
39
- type,
40
- rawType,
41
- required,
42
- ...(values && { values }),
43
- ...(defaultValue !== undefined && { defaultValue }),
44
- ...(doc && { doc })
45
- });
11
+ const seen = new Set();
12
+ // Fields a more derived class initializes without declaring them an input.
13
+ // Angular keeps the inherited input, but the component starts out with that value.
14
+ const reinitialized = new Map();
15
+ for (const { classDecl: owner, definesMetadata } of hierarchy.classes) {
16
+ const subclass = owner === classDecl ? undefined : classDecl;
17
+ for (const member of owner.members) {
18
+ if (!ts.isPropertyDeclaration(member) || isStatic(member))
19
+ continue;
20
+ const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
21
+ if (!name || seen.has(name))
22
+ continue;
23
+ const input = definesMetadata ? readInput(member, name, checker, owner, subclass) : undefined;
24
+ if (!input) {
25
+ if (member.initializer && !reinitialized.has(name))
26
+ reinitialized.set(name, member);
27
+ continue;
28
+ }
29
+ const initializedBy = reinitialized.get(name);
30
+ if (initializedBy) {
31
+ const defaultValue = readInitialValue(initializedBy, checker);
32
+ if (defaultValue === undefined)
33
+ delete input.defaultValue;
34
+ else
35
+ input.defaultValue = defaultValue;
36
+ }
37
+ seen.add(name);
38
+ inputs.push(input);
46
39
  }
47
40
  }
48
41
  return inputs;
49
42
  }
43
+ function isStatic(member) {
44
+ return !!ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword);
45
+ }
46
+ /** What a field starts out with: its initializer, or for `input()` and `model()` the value passed to them. */
47
+ function readInitialValue(member, checker) {
48
+ const signalCall = getInputSignalCall(member);
49
+ if (!signalCall)
50
+ return member.initializer ? evaluateDefault(member.initializer, checker) : undefined;
51
+ return !isSignalInputRequired(signalCall) && signalCall.arguments.length > 0 ? evaluateDefault(signalCall.arguments[0], checker) : undefined;
52
+ }
50
53
  /**
51
- * Extract all @Output() and output() signal metadata from a class declaration.
54
+ * `owner` declares the member. `subclass` is the showcased class when that is
55
+ * a different one, which decides what the type parameters of `owner` stand for.
52
56
  */
53
- export function extractOutputs(classDecl, checker) {
57
+ function readInput(member, name, checker, owner, subclass) {
58
+ const inputDecorator = findDecorator(member, 'Input');
59
+ if (inputDecorator) {
60
+ const required = isDecoratorInputRequired(inputDecorator);
61
+ const defaultValue = readInitialValue(member, checker);
62
+ const doc = getJsDocComment(member, checker);
63
+ const instantiated = subclass && usesTypeParameters(member.type ?? member.initializer, owner, checker) ? getInstantiatedType(name, subclass, checker) : undefined;
64
+ const { type, values, rawType } = instantiated ? mapType(instantiated, checker) : resolveDecoratorInputType(member, checker);
65
+ return {
66
+ name,
67
+ type,
68
+ rawType,
69
+ required,
70
+ ...(values && { values }),
71
+ ...(defaultValue !== undefined && { defaultValue }),
72
+ ...(doc && { doc })
73
+ };
74
+ }
75
+ const signalCall = getInputSignalCall(member);
76
+ if (!signalCall)
77
+ return undefined;
78
+ const required = isSignalInputRequired(signalCall);
79
+ const defaultValue = readInitialValue(member, checker);
80
+ const doc = getJsDocComment(member, checker);
81
+ const declaredType = signalCall.typeArguments?.[0] ?? signalCall.arguments[0];
82
+ // A signal is a getter function, so its call signature returns the value type.
83
+ const instantiated = subclass && usesTypeParameters(declaredType, owner, checker) ? getInstantiatedType(name, subclass, checker)?.getCallSignatures()[0]?.getReturnType() : undefined;
84
+ const { type, values, rawType } = instantiated ? mapType(instantiated, checker) : resolveSignalInputType(signalCall, checker);
85
+ return {
86
+ name,
87
+ type,
88
+ rawType,
89
+ required,
90
+ ...(values && { values }),
91
+ ...(defaultValue !== undefined && { defaultValue }),
92
+ ...(doc && { doc })
93
+ };
94
+ }
95
+ /**
96
+ * Extract all @Output() and output() signal metadata from a class declaration
97
+ * and the base classes Angular inherits outputs from.
98
+ */
99
+ export function extractOutputs(classDecl, checker, hierarchy = getClassHierarchy(classDecl, checker)) {
54
100
  const outputs = [];
55
- for (const member of classDecl.members) {
56
- if (!ts.isPropertyDeclaration(member))
57
- continue;
58
- const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
59
- if (!name)
60
- continue;
61
- const outputDecorator = findDecorator(member, 'Output');
62
- if (outputDecorator) {
63
- const doc = getJsDocComment(member, checker);
64
- outputs.push({ name, ...(doc && { doc }) });
101
+ const seen = new Set();
102
+ for (const { classDecl: owner, definesMetadata } of hierarchy.classes) {
103
+ if (!definesMetadata)
65
104
  continue;
66
- }
67
- if (isOutputSignal(member)) {
105
+ for (const member of owner.members) {
106
+ if (!ts.isPropertyDeclaration(member))
107
+ continue;
108
+ const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined;
109
+ if (!name || seen.has(name))
110
+ continue;
111
+ if (!findDecorator(member, 'Output') && !isOutputSignal(member))
112
+ continue;
68
113
  const doc = getJsDocComment(member, checker);
114
+ seen.add(name);
69
115
  outputs.push({ name, ...(doc && { doc }) });
70
116
  }
71
117
  }
72
118
  return outputs;
73
119
  }
120
+ /**
121
+ * Whether a declared type, or the expression a type is inferred from,
122
+ * mentions a type parameter of the class that declares it. Only then does the
123
+ * subclass change the type; otherwise the source text stays, alias names included.
124
+ */
125
+ function usesTypeParameters(node, owner, checker) {
126
+ const parameters = owner.typeParameters;
127
+ if (!node || !parameters?.length)
128
+ return false;
129
+ const isOwnParameter = (declaration) => !!declaration && ts.isTypeParameterDeclaration(declaration) && parameters.includes(declaration);
130
+ const visit = (child) => {
131
+ if (ts.isTypeReferenceNode(child) && ts.isIdentifier(child.typeName) && isOwnParameter(checker.getSymbolAtLocation(child.typeName)?.declarations?.[0])) {
132
+ return true;
133
+ }
134
+ return ts.forEachChild(child, visit) ?? false;
135
+ };
136
+ // An inferred type can depend on `T` without the source naming it: `model(this.initial)`.
137
+ return visit(node) || (!ts.isTypeNode(node) && typeMentions(checker.getTypeAtLocation(node), isOwnParameter, checker));
138
+ }
139
+ function typeMentions(type, isOwnParameter, checker, visited = new Set()) {
140
+ if (visited.has(type))
141
+ return false;
142
+ visited.add(type);
143
+ if (type.flags & ts.TypeFlags.TypeParameter)
144
+ return isOwnParameter(type.symbol?.declarations?.[0]);
145
+ const isReference = type.flags & ts.TypeFlags.Object && type.objectFlags & ts.ObjectFlags.Reference;
146
+ const nested = [
147
+ ...(type.isUnionOrIntersection() ? type.types : []),
148
+ ...(type.aliasTypeArguments ?? []),
149
+ ...(isReference ? checker.getTypeArguments(type) : [])
150
+ ];
151
+ return nested.some((t) => typeMentions(t, isOwnParameter, checker, visited));
152
+ }
153
+ /**
154
+ * The type of a property declared in a generic base class, as the subclass
155
+ * sees it: `items = input.required<T[]>()` in `Field<T>` is an
156
+ * `InputSignal<Country[]>` on `Picker extends Field<Country>`.
157
+ */
158
+ function getInstantiatedType(name, subclass, checker) {
159
+ const property = checker.getPropertyOfType(checker.getTypeAtLocation(subclass), name);
160
+ return property && checker.getTypeOfSymbolAtLocation(property, subclass);
161
+ }
162
+ /**
163
+ * A default is all or nothing. The renderer applies it to the component, so
164
+ * the part of `{ ...shared, size: 'm' }` that can be read would overwrite the
165
+ * whole default the component actually declares.
166
+ */
167
+ function evaluateDefault(node, checker) {
168
+ let complete = true;
169
+ const value = evaluateStatic(node, { checker, report: () => (complete = false) });
170
+ return complete ? value : undefined;
171
+ }
74
172
  function getInputSignalCall(member) {
75
173
  const init = member.initializer;
76
174
  if (!init || !ts.isCallExpression(init))
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export function generateManifest(components) {
5
5
  const lines = [
6
- '// AUTO-GENERATED by ng-prism — do not edit!',
6
+ '// AUTO-GENERATED by ng-prism. Do not edit!',
7
7
  "import type { PrismManifest } from 'ng-prism/plugin';",
8
8
  '',
9
9
  'export const PRISM_MANIFEST: PrismManifest = {',
@@ -30,6 +30,10 @@ function json(value) {
30
30
  function indent(depth) {
31
31
  return ' '.repeat(depth);
32
32
  }
33
+ /** `'aria-label'` and content slots like `'[card-header]'` are only valid keys in quotes. */
34
+ function formatKey(key) {
35
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
36
+ }
33
37
  function formatObject(obj, baseIndent) {
34
38
  const entries = Object.entries(obj).filter(([, v]) => v !== undefined);
35
39
  if (entries.length === 0)
@@ -41,7 +45,7 @@ function formatObject(obj, baseIndent) {
41
45
  : typeof value === 'object' && value !== null
42
46
  ? formatObject(value, inner)
43
47
  : json(value);
44
- return `${indent(inner)}${key}: ${formatted},`;
48
+ return `${indent(inner)}${formatKey(key)}: ${formatted},`;
45
49
  });
46
50
  return `{\n${lines.join('\n')}\n${indent(baseIndent)}}`;
47
51
  }
@@ -7,6 +7,8 @@ export interface CreateScannerOptions {
7
7
  }
8
8
  export interface ScanResult {
9
9
  components: ScannedComponent[];
10
+ /** One message per @Showcase value the scan had to drop, already printed as a warning. */
11
+ diagnostics: string[];
10
12
  }
11
13
  export interface Scanner {
12
14
  /** Scan all entry points. Reuses TypeScript program state from previous scans. */
@@ -16,7 +18,7 @@ export interface Scanner {
16
18
  * Create a stateful multi-entry scanner that retains the previous ts.Program between scans.
17
19
  * A single shared program is created with every entry-point file as a root name, so the
18
20
  * transitive type graph (Angular framework typings, etc.) is parsed once and shared via
19
- * the program's `TypeChecker` — instead of N programs that each load Material/CDK/RxJS.
21
+ * the program's `TypeChecker`, instead of N programs that each load Material/CDK/RxJS.
20
22
  *
21
23
  * TypeScript reuses parsed SourceFile objects from the old program on rebuild, making
22
24
  * incremental scans fast.
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../../src/builder/scanner/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAAE,KAAK,eAAe,EAA4B,MAAM,0BAA0B,CAAC;AAE1F,MAAM,WAAW,oBAAoB;IACjC,WAAW,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC5C,eAAe,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,OAAO;IACpB,kFAAkF;IAClF,IAAI,IAAI,UAAU,CAAC;CACtB;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CA+CpE"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../../src/builder/scanner/scanner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAAE,KAAK,eAAe,EAA4B,MAAM,0BAA0B,CAAC;AAE1F,MAAM,WAAW,oBAAoB;IACjC,WAAW,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC5C,eAAe,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACvB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B,0FAA0F;IAC1F,WAAW,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACpB,kFAAkF;IAClF,IAAI,IAAI,UAAU,CAAC;CACtB;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAiDpE"}
@@ -13,14 +13,14 @@ const DEFAULT_COMPILER_OPTIONS = {
13
13
  types: [],
14
14
  // Perf: we never call program.emit(); skip emit-related setup work.
15
15
  noEmit: true,
16
- // Perf: complement to skipLibCheck — also skip type-checking lib.*.d.ts files.
16
+ // Perf: complements skipLibCheck by also skipping type-checking of lib.*.d.ts files.
17
17
  skipDefaultLibCheck: true
18
18
  };
19
19
  /**
20
20
  * Create a stateful multi-entry scanner that retains the previous ts.Program between scans.
21
21
  * A single shared program is created with every entry-point file as a root name, so the
22
22
  * transitive type graph (Angular framework typings, etc.) is parsed once and shared via
23
- * the program's `TypeChecker` — instead of N programs that each load Material/CDK/RxJS.
23
+ * the program's `TypeChecker`, instead of N programs that each load Material/CDK/RxJS.
24
24
  *
25
25
  * TypeScript reuses parsed SourceFile objects from the old program on rebuild, making
26
26
  * incremental scans fast.
@@ -37,8 +37,10 @@ export function createScanner(options) {
37
37
  previousProgram = program;
38
38
  const checker = program.getTypeChecker();
39
39
  const allComponents = [];
40
+ const diagnostics = [];
41
+ const notices = new Set();
40
42
  for (const { exports, importPath } of entries) {
41
- const components = scanComponents(exports, checker);
43
+ const components = scanComponents(exports, checker, diagnostics, notices);
42
44
  for (const c of components)
43
45
  c.importPath = importPath;
44
46
  allComponents.push(...components);
@@ -59,7 +61,7 @@ export function createScanner(options) {
59
61
  seen.add(key);
60
62
  unique.push(c);
61
63
  }
62
- return { components: unique };
64
+ return { components: unique, diagnostics };
63
65
  }
64
66
  };
65
67
  }