@oneuptime/common 14.0.3 → 14.0.6

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 (663) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  3. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  4. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  5. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  6. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  9. package/Models/DatabaseModels/Incident.ts +55 -0
  10. package/Models/DatabaseModels/Index.ts +16 -0
  11. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  13. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  14. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  18. package/Server/Middleware/MultipartFormData.ts +1 -1
  19. package/Server/Middleware/TelemetryIngest.ts +15 -8
  20. package/Server/Services/AlertReminderRuleService.ts +100 -24
  21. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  22. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  23. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  24. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  25. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  26. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  27. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  28. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  29. package/Server/Services/DatabaseServerService.ts +4546 -0
  30. package/Server/Services/DatabaseService.ts +14 -8
  31. package/Server/Services/DockerResourceService.ts +43 -2
  32. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  33. package/Server/Services/Index.ts +16 -0
  34. package/Server/Services/InventoryItemService.ts +113 -5
  35. package/Server/Services/IoTDeviceService.ts +145 -37
  36. package/Server/Services/MetricService.ts +1 -1
  37. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  38. package/Server/Services/PodmanResourceService.ts +43 -2
  39. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  40. package/Server/Services/ServiceService.ts +1 -1
  41. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  42. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  43. package/Server/Services/UserService.ts +14 -1
  44. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  45. package/Server/Types/Database/QueryHelper.ts +84 -2
  46. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  47. package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
  48. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  49. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  50. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  51. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
  52. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  53. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
  54. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  55. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  56. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  57. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  58. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  59. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  60. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  61. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  62. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  63. package/Server/Utils/StartServer.ts +26 -6
  64. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  65. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  66. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  67. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  68. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  69. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  70. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  71. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  72. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  73. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  74. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  75. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  76. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  77. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  78. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  79. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  80. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  81. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  82. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  83. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  84. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  85. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  86. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  87. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  88. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  89. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  90. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  91. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  92. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  93. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  94. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  95. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  96. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  97. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  98. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  99. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  100. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  101. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  102. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  103. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  104. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  105. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  106. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  107. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  108. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  109. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  110. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  111. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  112. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  113. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  114. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  115. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  116. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  117. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  118. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  119. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  120. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  121. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  122. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  123. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  124. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  125. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  126. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  127. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  128. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  129. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  130. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  131. package/Tests/ClassStrings.ts +178 -0
  132. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  133. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  134. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  135. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  136. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  137. package/Tests/RenderedMarkup.ts +518 -0
  138. package/Tests/ResponsiveVisibility.test.ts +490 -4
  139. package/Tests/ResponsiveVisibility.ts +505 -52
  140. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  141. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  142. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  143. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  144. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  145. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  146. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  147. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  148. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  149. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  150. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  151. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  152. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  153. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  154. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  155. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  156. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  157. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  158. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  159. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  160. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  161. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  162. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  163. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  164. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  165. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  166. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  167. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  168. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  169. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  170. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  171. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  172. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  173. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  174. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  175. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  176. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  177. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  178. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  179. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  180. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  181. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  182. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  183. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  184. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  185. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  186. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  187. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  188. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  189. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  190. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  191. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  192. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  193. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  194. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  195. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  196. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  197. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  198. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  199. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
  200. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  201. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  202. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  203. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  204. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  205. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  206. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  207. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  208. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  209. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  210. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  211. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  212. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  213. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  214. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  215. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  216. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  217. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  218. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  219. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  220. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  221. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  222. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  223. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  224. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  225. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  226. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  227. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  228. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  229. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  230. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  231. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  232. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  233. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  234. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  235. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  236. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  237. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  238. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  239. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  240. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  241. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  242. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
  243. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
  244. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  245. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  246. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  247. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  248. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  249. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  250. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
  251. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  252. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  253. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  254. package/Tests/UI/Components/Card.test.tsx +93 -2
  255. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  256. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  257. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  258. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  259. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  260. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  261. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  262. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  263. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  264. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  265. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  266. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  267. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  268. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  269. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  270. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  271. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  272. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  273. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  274. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
  275. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
  276. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  277. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  278. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  279. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  280. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  281. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  282. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  283. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  284. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  285. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  286. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  287. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  288. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  289. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  290. package/Tests/tsconfig.json +16 -0
  291. package/Types/AI/AIResourceContext.ts +7 -0
  292. package/Types/BaseDatabase/AggregationType.ts +7 -0
  293. package/Types/Dashboard/DashboardTemplates.ts +22 -8
  294. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  295. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  296. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  297. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  298. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  299. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  300. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  301. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  302. package/Types/Email/EmailTemplateType.ts +1 -0
  303. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  304. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  305. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  306. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  307. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  308. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  309. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  310. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  311. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  312. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  313. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  314. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  315. package/Types/Permission.ts +267 -0
  316. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  317. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  318. package/Types/Rules/RuleRun.ts +12 -0
  319. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  320. package/Types/Rum/WebVitals.ts +13 -0
  321. package/Types/Telemetry/EntityType.ts +23 -0
  322. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  323. package/Types/Telemetry/ServiceType.ts +8 -0
  324. package/UI/Components/Banner/Banner.tsx +1 -1
  325. package/UI/Components/Card/Card.tsx +1 -1
  326. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  327. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  328. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  329. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  330. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  331. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  332. package/UI/Components/Forms/BasicForm.tsx +1 -1
  333. package/UI/Components/Header/Header.tsx +1 -1
  334. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  335. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  336. package/UI/Components/Input/Input.tsx +11 -0
  337. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  338. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  339. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  340. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  341. package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
  342. package/UI/Components/Navbar/NavBar.tsx +9 -2
  343. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  344. package/UI/Components/Page/Page.tsx +1 -1
  345. package/UI/Components/Pagination/Pagination.tsx +3 -3
  346. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  347. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  348. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  349. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  350. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  351. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  352. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  353. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  354. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  355. package/UI/Styles/Theme.css +8 -1
  356. package/UI/Utils/Navigation.ts +89 -42
  357. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  358. package/UI/Utils/TelemetryService.ts +4 -3
  359. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  360. package/Utils/LabelRuleImportExport.ts +2 -0
  361. package/Utils/MetricUnitUtil.ts +103 -11
  362. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  363. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  364. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  365. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  366. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  367. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  368. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  369. package/Utils/Telemetry/EntityKey.ts +176 -0
  370. package/Utils/ValueFormatter.ts +16 -0
  371. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  372. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  373. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  374. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  375. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  376. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  377. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  378. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  379. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  380. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  381. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  382. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  383. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  384. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  385. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  386. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  387. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  388. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  389. package/build/dist/Models/DatabaseModels/Index.js +16 -0
  390. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  391. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  393. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  395. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  397. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  398. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  399. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  400. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  401. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  402. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  403. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  404. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  405. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  406. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  407. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  408. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  409. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  410. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  411. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  412. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  413. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  414. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  415. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  416. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  417. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  418. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  419. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  420. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  421. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  422. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  423. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  424. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  425. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  426. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  427. package/build/dist/Server/Services/DatabaseService.js +8 -4
  428. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  429. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  430. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  431. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  432. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  433. package/build/dist/Server/Services/Index.js +16 -0
  434. package/build/dist/Server/Services/Index.js.map +1 -1
  435. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  436. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  437. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  438. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  439. package/build/dist/Server/Services/MetricService.js +1 -1
  440. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  441. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  442. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  443. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  444. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  445. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  446. package/build/dist/Server/Services/ServiceService.js +1 -1
  447. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  448. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  449. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  450. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  451. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  452. package/build/dist/Server/Services/UserService.js +13 -1
  453. package/build/dist/Server/Services/UserService.js.map +1 -1
  454. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  455. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  456. package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
  457. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  458. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  459. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  460. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
  461. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  462. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  463. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  464. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  465. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  466. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  467. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
  468. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  469. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  470. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  471. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  472. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  473. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  474. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  475. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  476. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  477. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  478. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  479. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  480. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  481. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  482. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  483. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  484. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  485. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  486. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  487. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  488. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  489. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  490. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  491. package/build/dist/Server/Utils/StartServer.js +20 -4
  492. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  493. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  494. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  495. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  496. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  497. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  498. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  499. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  500. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  501. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  502. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  503. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  504. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  505. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  506. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  507. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  508. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  509. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  510. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  512. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  513. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  514. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  515. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  516. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  517. package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
  518. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  519. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  520. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  521. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  522. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  523. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  524. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  525. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  526. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  527. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  528. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  529. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  530. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  531. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  532. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  533. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  534. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  535. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  536. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  537. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  538. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  539. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  540. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  541. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  542. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  543. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  544. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  545. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  546. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  547. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  548. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  549. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  550. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  551. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  552. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  553. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  554. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  555. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  556. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  557. package/build/dist/Types/Permission.js +234 -0
  558. package/build/dist/Types/Permission.js.map +1 -1
  559. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  560. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  561. package/build/dist/Types/Rules/RuleRun.js +4 -0
  562. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  563. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  564. package/build/dist/Types/Rum/WebVitals.js +12 -0
  565. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  566. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  567. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  568. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  569. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  570. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  571. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  572. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  573. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  574. package/build/dist/UI/Components/Card/Card.js +1 -1
  575. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  576. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  577. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  578. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  579. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  580. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  581. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  582. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  583. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  584. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  585. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  586. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  587. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  588. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  589. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  590. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  591. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  592. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  593. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  594. package/build/dist/UI/Components/Input/Input.js +1 -1
  595. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  596. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  597. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  598. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  600. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  601. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  602. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  603. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  604. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
  605. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  606. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  607. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  608. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  609. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  610. package/build/dist/UI/Components/Page/Page.js +1 -1
  611. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  612. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  613. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  614. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  615. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  616. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  617. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  618. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  619. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  620. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  621. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  622. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  626. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  627. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  628. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  629. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  630. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  631. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  632. package/build/dist/UI/Utils/Navigation.js +66 -33
  633. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  634. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  635. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  636. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  637. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  638. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  639. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  640. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  641. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  642. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  643. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  644. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  645. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  646. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  647. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  648. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  649. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  650. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  651. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  652. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  653. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  654. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  655. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  656. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  657. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  658. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  659. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  660. package/build/dist/Utils/ValueFormatter.js +15 -0
  661. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  662. package/package.json +1 -1
  663. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2152 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import ts from "typescript";
4
+ import { isMediaQueryVariant, splitVariants } from "./ResponsiveVisibility";
5
+
6
+ /*
7
+ * The detector behind the "no class string leans on a variant to undo a bare
8
+ * `hidden`" guard (Tests/UI/ForeignHiddenRuleGuard.test.ts).
9
+ *
10
+ * Why the rule exists: a customer's dashboard rendered with no navigation bar
11
+ * at all - no Home, no Products menu, so no way into any product - because
12
+ * their browser carried `.hidden{display:none !important}`. Bootstrap 3 and
13
+ * HTML5 Boilerplate ship exactly that rule, and browser extensions and user
14
+ * stylesheets inject it into every page they touch. It matches the bare
15
+ * `hidden` class and outranks every responsive or state utility we emit (it
16
+ * is !important, or merely appended after Tailwind's <style> at equal
17
+ * specificity), so an element written `hidden md:flex` stayed hidden at every
18
+ * width. `max-md:hidden md:flex` displays identically everywhere and never
19
+ * carries the class the foreign rule targets.
20
+ *
21
+ * What it flags:
22
+ *
23
+ * 1. One string - a string literal, a template without substitutions, or
24
+ * the static text of a template literal - whose class tokens include the
25
+ * bare `hidden` AND a variant-prefixed display utility that could show
26
+ * the element again: md:flex, max-lg:block, group-hover:flex,
27
+ * peer-checked:inline, [&.open]:grid, dark:block ...
28
+ * 2. The same pair split across the pieces of one class expression: a
29
+ * JSX attribute or a variable / property / parameter whose name matches
30
+ * /class/i, a clsx / classNames / cn / cx / twMerge / twJoin call, or a
31
+ * template literal with the pieces in its substitutions. Only pieces
32
+ * that can end up in the class value together count: the two branches
33
+ * of a ternary (or of || and ??) never meet, a ternary's condition is
34
+ * not class text, and an object literal holds classes for different
35
+ * elements.
36
+ * 3. The `sr-only` analogue of both: a bare `sr-only` that a media-query
37
+ * `not-sr-only` undoes (`sr-only sm:not-sr-only`). Bootstrap 3 and HTML5
38
+ * Boilerplate define `.sr-only` too, and appended after Tailwind's
39
+ * <style> it wins on source order, collapsing a label that is meant to
40
+ * be visible from `sm` up to 1px at every width. See
41
+ * isSrOnlyUnhidingToken for why state variants are left alone.
42
+ * 4. All of the above in the server-rendered views (EJS and HTML), which
43
+ * load the same Play CDN: the static text of every class attribute,
44
+ * split at its `<% %>` tags, and the JavaScript inside every tag (see
45
+ * analyzeMarkup).
46
+ *
47
+ * What it deliberately lets through: a lone `hidden` (toggled from
48
+ * JavaScript - the foreign rule can only hide more, never less), `md:hidden`,
49
+ * `max-md:hidden md:flex`, `flex lg:hidden`, `overflow-hidden`, a `sr-only`
50
+ * shown again by a state variant (`sr-only focus:not-sr-only`, the skip-link
51
+ * idiom), and anything in a comment or in JSX text, because only real string
52
+ * syntax is read.
53
+ * Tokens glued to a `${...}` substitution (or to a `<% %>` tag) are not whole
54
+ * classes and are not read either.
55
+ */
56
+
57
+ /*
58
+ * Tailwind's display utilities, `hidden` included. Kept in step with the
59
+ * list in ResponsiveVisibility.ts.
60
+ */
61
+ const DISPLAY_UTILITIES: ReadonlySet<string> = new Set<string>([
62
+ "block",
63
+ "inline-block",
64
+ "inline",
65
+ "flex",
66
+ "inline-flex",
67
+ "table",
68
+ "inline-table",
69
+ "table-caption",
70
+ "table-cell",
71
+ "table-column",
72
+ "table-column-group",
73
+ "table-footer-group",
74
+ "table-header-group",
75
+ "table-row-group",
76
+ "table-row",
77
+ "flow-root",
78
+ "grid",
79
+ "inline-grid",
80
+ "contents",
81
+ "list-item",
82
+ "hidden",
83
+ ]);
84
+
85
+ const BREAKPOINTS: ReadonlyArray<string> = ["sm", "md", "lg", "xl", "2xl"];
86
+
87
+ // The helpers whose arguments are all class text.
88
+ const CLASS_HELPERS: ReadonlySet<string> = new Set<string>([
89
+ "clsx",
90
+ "classnames",
91
+ "classNames",
92
+ "cn",
93
+ "cx",
94
+ "twMerge",
95
+ "twJoin",
96
+ ]);
97
+
98
+ // Methods that pass class text through: [...].join(" "), `...`.trim().
99
+ const PASS_THROUGH_METHODS: ReadonlySet<string> = new Set<string>([
100
+ "join",
101
+ "trim",
102
+ "concat",
103
+ ]);
104
+
105
+ const CLASS_NAME_PATTERN: RegExp = /class/i;
106
+
107
+ // Markup the guard reads as views.
108
+ const MARKUP_FILE_PATTERN: RegExp = /\.(ejs|html?)$/i;
109
+
110
+ const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set<string>([
111
+ "node_modules",
112
+ "dist",
113
+ "build",
114
+ ]);
115
+
116
+ // The hand-converted state-variant sites the failure message points at.
117
+ export const GROUP_HOVER_EXAMPLE_FILE: string =
118
+ "packages/Common/UI/Components/LogsViewer/components/FacetValueRow.tsx";
119
+ export const DISCLOSURE_EXAMPLE_FILE: string =
120
+ "packages/App/FeatureSet/Dashboard/src/Pages/Runbook/View/ExecutionView.tsx";
121
+ export const NAMED_GROUP_EXAMPLE_FILE: string =
122
+ "packages/Common/UI/Components/StatusPage/ResourceGroupNavigator.tsx";
123
+ // Brightens group-hover reveals in dark mode, keyed on class substrings.
124
+ export const THEME_STYLESHEET_FILE: string =
125
+ "packages/Common/UI/Styles/Theme.css";
126
+
127
+ /*
128
+ * What every `group-[:not(:hover)]` rewrite has to be checked for. The
129
+ * inversion is not free with nested groups: `group-hover:flex` shows while ANY
130
+ * enclosing `.group` is hovered - in effect the outermost - and
131
+ * `group-[:not(:hover)]:hidden` hides while any is NOT, so it shows only while
132
+ * the innermost one is. And the dark theme keys its hover brightening on the
133
+ * class text, so a rewrite that changes that text can change the colour.
134
+ */
135
+ export const GROUP_REWRITE_CAVEAT: string = `\`group-hover:\` shows the element while any \`.group\` around it is hovered and \`group-[:not(:hover)]:hidden\` hides it while any is not, so the two agree exactly when one \`.group\` encloses it; with nested groups the rewrite follows the innermost \`.group\`, which is right only when that group is the intended trigger - for an outer trigger, name it: \`group/card\` on the trigger and \`group-[:not(:hover)]/card:hidden\` on the element (as ${NAMED_GROUP_EXAMPLE_FILE} does). In dark mode ${THEME_STYLESHEET_FILE} brightens gray text revealed under a hovered \`.group\` by class substring and already recognises \`group-[:not(:hover)\` beside \`group-hover\`, so the unnamed rewrite keeps that colour; a named \`group/<name>\` trigger is not a \`.group\` to that rule (it was not for \`group-hover/<name>:\` either)`;
136
+
137
+ export const STATE_VARIANT_ADVICE: string = `no breakpoint rewrite exists for this variant: show the element by default and hide it under the negated state instead - \`flex ... group-[:not(:hover)]:hidden\` (${GROUP_HOVER_EXAMPLE_FILE}) or, for a <details> disclosure, the structural \`inline [details:not([open])>summary>&]:hidden\` (${DISCLOSURE_EXAMPLE_FILE}). ${GROUP_REWRITE_CAVEAT}`;
138
+
139
+ // The advice that goes with the mechanical `hidden ... group-hover:<display>` inversion.
140
+ export const GROUP_HOVER_REWRITE_ADVICE: string = `show it by default and hide it while the group is not hovered, as ${GROUP_HOVER_EXAMPLE_FILE} does. ${GROUP_REWRITE_CAVEAT}`;
141
+
142
+ /*
143
+ * For a `not-sr-only` behind a media query that is not one of the named
144
+ * screens (print:, motion-reduce:, min-[900px]: ...): state variants never
145
+ * get here, since they out-specify the foreign `.sr-only` (see
146
+ * isSrOnlyUnhidingToken).
147
+ */
148
+ export const SR_ONLY_MEDIA_ADVICE: string =
149
+ "no named-breakpoint rewrite exists for this media variant: keep the `not-sr-only` and apply `sr-only` only under the opposite media query instead of as the bare class - `max-[900px]:sr-only min-[900px]:not-sr-only` for `sr-only min-[900px]:not-sr-only` - so the element never carries the class a foreign `.sr-only` targets";
150
+
151
+ export type HidingClass = "hidden" | "sr-only";
152
+
153
+ function mixedBreakpointAdvice(hidingClass: HidingClass): string {
154
+ const unhiders: string =
155
+ hidingClass === "hidden" ? "display utilities" : "`not-sr-only` utilities";
156
+
157
+ return `the utilities that show it mix min-width and max-width screens, so no single rewrite of the bare \`${hidingClass}\` is exact: keep the ${unhiders} and hide it with \`max-<bp>:${hidingClass}\` / \`<bp>:${hidingClass}\` for exactly the ranges it should be hidden in`;
158
+ }
159
+
160
+ function disagreeingPiecesAdvice(hidingClass: HidingClass): string {
161
+ return `the pieces that show it never meet (they are alternative branches) and un-hide it at different screens, so no single rewrite of the bare \`${hidingClass}\` is exact: move the \`max-<bp>:${hidingClass}\` into each branch, next to the utility it pairs with`;
162
+ }
163
+
164
+ function conditionalPartnerAdvice(hidingClass: HidingClass): string {
165
+ const pair: string =
166
+ hidingClass === "hidden"
167
+ ? "max-<bp>:hidden <bp>:<display>"
168
+ : "max-<bp>:sr-only <bp>:not-sr-only";
169
+
170
+ return `the utility that shows it is only there under a condition the bare \`${hidingClass}\` does not share, so rewriting the \`${hidingClass}\` alone would show the element whenever that condition is off: move the pair into the condition together - \`condition ? "${pair}" : "${hidingClass}"\` - so a lone \`${hidingClass}\` is all that is left when it is off`;
171
+ }
172
+
173
+ export const MIXED_BREAKPOINT_ADVICE: string = mixedBreakpointAdvice("hidden");
174
+ export const DISAGREEING_PIECES_ADVICE: string =
175
+ disagreeingPiecesAdvice("hidden");
176
+ export const CONDITIONAL_PARTNER_ADVICE: string =
177
+ conditionalPartnerAdvice("hidden");
178
+ export const SR_ONLY_MIXED_BREAKPOINT_ADVICE: string =
179
+ mixedBreakpointAdvice("sr-only");
180
+ export const SR_ONLY_DISAGREEING_PIECES_ADVICE: string =
181
+ disagreeingPiecesAdvice("sr-only");
182
+ export const SR_ONLY_CONDITIONAL_PARTNER_ADVICE: string =
183
+ conditionalPartnerAdvice("sr-only");
184
+
185
+ export const WHY_PARAGRAPH: string =
186
+ "Why: a customer's dashboard rendered with no navigation bar (no Home, no Products menu, so no way into any product) because their browser carried `.hidden{display:none !important}` - Bootstrap 3 and HTML5 Boilerplate ship exactly that rule, and browser extensions and user stylesheets inject it into pages they touch. It matches the bare `hidden` class and beats every responsive or state utility we emit (it is !important, or merely appended after Tailwind's <style> at equal specificity), so an element written `hidden md:flex` stays hidden at every width. `max-md:hidden md:flex` displays identically at every width and never carries the class that rule targets. A lone `hidden` toggled from JavaScript is fine: the foreign rule can only hide more.";
187
+
188
+ export const SR_ONLY_WHY_PARAGRAPH: string =
189
+ "Why `sr-only` too: Bootstrap 3 and HTML5 Boilerplate also define `.sr-only`, and a sheet carrying it that is appended after Tailwind's <style> ties with a media-query utility on specificity (an @media wrapper adds none) and wins on source order, so a label written `sr-only sm:not-sr-only` stays a 1px screen-reader-only box at every width. `max-sm:sr-only sm:not-sr-only` renders identically and never carries that class. A state variant is fine - a skip link written `sr-only focus:not-sr-only`, a `group-hover:not-sr-only` - because its selector out-specifies the lone class of a foreign `.sr-only`, which neither library marks !important.";
190
+
191
+ export type FindingKind = "same-string" | "cross-piece";
192
+
193
+ export interface ForeignHiddenRuleFinding {
194
+ // As passed to analyzeSource (the scan passes a repository-relative path).
195
+ file: string;
196
+ // Markup: the line of the class attribute. Code: the line the string starts on.
197
+ line: number;
198
+ kind: FindingKind;
199
+ // The class a foreign rule targets: `hidden`, or its `sr-only` analogue.
200
+ hidingClass: HidingClass;
201
+ // The string that carries the bare `hidden` (or `sr-only`), as written.
202
+ text: string;
203
+ // cross-piece: the other piece of the class expression that shows it again.
204
+ partnerText: string | null;
205
+ // The variant utilities that show the element again.
206
+ unhidingTokens: Array<string>;
207
+ // What to write instead of `text`, when a mechanical rewrite is exact.
208
+ replacement: string | null;
209
+ advice: string;
210
+ }
211
+
212
+ // A class string that hides with a variant-prefixed `hidden` (the safe forms).
213
+ export interface VariantHiddenSite {
214
+ file: string;
215
+ line: number;
216
+ text: string;
217
+ }
218
+
219
+ export interface SourceAnalysis {
220
+ findings: Array<ForeignHiddenRuleFinding>;
221
+ variantHiddenSites: Array<VariantHiddenSite>;
222
+ }
223
+
224
+ export interface ForeignHiddenRuleScan extends SourceAnalysis {
225
+ files: Array<string>;
226
+ }
227
+
228
+ interface ParsedToken {
229
+ variants: Array<string>;
230
+ utility: string;
231
+ }
232
+
233
+ /*
234
+ * One piece of class text. A template literal (or a class attribute) is one
235
+ * unit: its static parts always meet in the same value. `parts` has one more
236
+ * entry than `substitutions`, which hold each `${...}` or `<% %>` as it is to
237
+ * be printed.
238
+ */
239
+ interface ClassTextUnit {
240
+ // The string's node; null for the static text of a markup class attribute.
241
+ node: ts.Node | null;
242
+ parts: Array<string>;
243
+ substitutions: Array<string>;
244
+ tokens: Array<string>;
245
+ }
246
+
247
+ interface Branch {
248
+ node: ts.Node;
249
+ side: number;
250
+ }
251
+
252
+ interface PlacedUnit {
253
+ unit: ClassTextUnit;
254
+ branches: Array<Branch>;
255
+ }
256
+
257
+ interface Rewrite {
258
+ replacement: string | null;
259
+ advice: string;
260
+ }
261
+
262
+ interface BreakpointFix {
263
+ direction: "min" | "max";
264
+ breakpointIndex: number;
265
+ }
266
+
267
+ /*
268
+ * A class a foreign stylesheet can pin, and what undoes it in our markup.
269
+ * Everything else - the breakpoint arithmetic, the cross-piece reading, the
270
+ * failure message - is shared.
271
+ */
272
+ interface HidingClassRule {
273
+ hidingClass: HidingClass;
274
+ isUnhiding: (token: string) => boolean;
275
+ /*
276
+ * When no breakpoint rewrite exists: a state variant for `hidden`, a
277
+ * media query other than a named screen for `sr-only`.
278
+ */
279
+ noBreakpointAdvice: string;
280
+ mixedBreakpointAdvice: string;
281
+ disagreeingPiecesAdvice: string;
282
+ conditionalPartnerAdvice: string;
283
+ }
284
+
285
+ /* A file's path relative to baseDir, with "/" separators on every platform. */
286
+ export function toRelativePath(baseDir: string, filePath: string): string {
287
+ return path.relative(baseDir, filePath).split(path.sep).join("/");
288
+ }
289
+
290
+ /*
291
+ * The directories the guard reads: every feature set's browser source, the
292
+ * shared UI, and the Enterprise screens when ee/ is in the checkout (the
293
+ * Community Edition checkout, and the Common Test job, have no ee/).
294
+ */
295
+ export function listScanRoots(repositoryRoot: string): Array<string> {
296
+ const featureSetDir: string = path.join(
297
+ repositoryRoot,
298
+ "packages",
299
+ "App",
300
+ "FeatureSet",
301
+ );
302
+
303
+ const roots: Array<string> = fs
304
+ .readdirSync(featureSetDir, { withFileTypes: true })
305
+ .filter((entry: fs.Dirent): boolean => {
306
+ return entry.isDirectory();
307
+ })
308
+ .map((entry: fs.Dirent): string => {
309
+ return path.join(featureSetDir, entry.name, "src");
310
+ })
311
+ .filter((directory: string): boolean => {
312
+ return fs.existsSync(directory);
313
+ })
314
+ .sort();
315
+
316
+ roots.push(path.join(repositoryRoot, "packages", "Common", "UI"));
317
+
318
+ for (const enterpriseScreen of ["Dashboard", "AdminDashboard"]) {
319
+ const directory: string = path.join(repositoryRoot, "ee", enterpriseScreen);
320
+
321
+ if (fs.existsSync(directory)) {
322
+ roots.push(directory);
323
+ }
324
+ }
325
+
326
+ return roots;
327
+ }
328
+
329
+ /* Every .ts / .tsx module under a directory, declaration files excluded. */
330
+ export function listSourceFiles(directory: string): Array<string> {
331
+ const found: Array<string> = [];
332
+
333
+ if (!fs.existsSync(directory)) {
334
+ return found;
335
+ }
336
+
337
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
338
+ const fullPath: string = path.join(directory, entry.name);
339
+
340
+ if (entry.isDirectory()) {
341
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
342
+ found.push(...listSourceFiles(fullPath));
343
+ }
344
+ continue;
345
+ }
346
+
347
+ if (
348
+ entry.isFile() &&
349
+ (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
350
+ !entry.name.endsWith(".d.ts")
351
+ ) {
352
+ found.push(fullPath);
353
+ }
354
+ }
355
+
356
+ return found;
357
+ }
358
+
359
+ function isDirectory(directory: string): boolean {
360
+ try {
361
+ // statSync follows a symlinked directory, which realpath then dedupes.
362
+ return fs.statSync(directory).isDirectory();
363
+ } catch {
364
+ return false;
365
+ }
366
+ }
367
+
368
+ /*
369
+ * The children of `parent` whose name is `lowercaseName` in any case. Read
370
+ * from the listing rather than guessed: git tracks Docs/Views but
371
+ * APIReference/views, and on a case-insensitive filesystem a guessed
372
+ * `views` and `Views` would both exist and name the same directory.
373
+ */
374
+ function childDirectoriesNamed(
375
+ parent: string,
376
+ lowercaseName: string,
377
+ ): Array<string> {
378
+ if (!isDirectory(parent)) {
379
+ return [];
380
+ }
381
+
382
+ return fs
383
+ .readdirSync(parent, { withFileTypes: true })
384
+ .filter((entry: fs.Dirent): boolean => {
385
+ return (
386
+ entry.name.toLowerCase() === lowercaseName &&
387
+ isDirectory(path.join(parent, entry.name))
388
+ );
389
+ })
390
+ .map((entry: fs.Dirent): string => {
391
+ return path.join(parent, entry.name);
392
+ })
393
+ .sort();
394
+ }
395
+
396
+ /*
397
+ * The server-rendered views the guard reads, which load the same Tailwind
398
+ * Play CDN as the React frontends: every feature set's views (Docs, API
399
+ * Reference, the frontends' index.ejs shells, Identity) and public/ HTML,
400
+ * Common's server views and the website (packages/Home/Views). Deduped by
401
+ * real path, so a symlink or a case-insensitive filesystem cannot read a
402
+ * directory twice.
403
+ */
404
+ export function listViewRoots(repositoryRoot: string): Array<string> {
405
+ const featureSetDir: string = path.join(
406
+ repositoryRoot,
407
+ "packages",
408
+ "App",
409
+ "FeatureSet",
410
+ );
411
+
412
+ const candidates: Array<string> = [];
413
+
414
+ for (const featureSet of childDirectoriesOf(featureSetDir)) {
415
+ candidates.push(
416
+ ...childDirectoriesNamed(featureSet, "views"),
417
+ ...childDirectoriesNamed(featureSet, "public"),
418
+ );
419
+ }
420
+
421
+ candidates.push(
422
+ ...childDirectoriesNamed(
423
+ path.join(repositoryRoot, "packages", "Common", "Server"),
424
+ "views",
425
+ ),
426
+ ...childDirectoriesNamed(
427
+ path.join(repositoryRoot, "packages", "Home"),
428
+ "views",
429
+ ),
430
+ );
431
+
432
+ const seen: Set<string> = new Set<string>();
433
+
434
+ return candidates.filter((directory: string): boolean => {
435
+ const realPath: string = fs.realpathSync(directory);
436
+
437
+ if (seen.has(realPath)) {
438
+ return false;
439
+ }
440
+
441
+ seen.add(realPath);
442
+ return true;
443
+ });
444
+ }
445
+
446
+ function childDirectoriesOf(parent: string): Array<string> {
447
+ if (!isDirectory(parent)) {
448
+ return [];
449
+ }
450
+
451
+ return fs
452
+ .readdirSync(parent, { withFileTypes: true })
453
+ .map((entry: fs.Dirent): string => {
454
+ return path.join(parent, entry.name);
455
+ })
456
+ .filter(isDirectory)
457
+ .sort();
458
+ }
459
+
460
+ /* Markup the guard reads as views: EJS templates and plain HTML. */
461
+ export function isMarkupFile(fileName: string): boolean {
462
+ return MARKUP_FILE_PATTERN.test(fileName);
463
+ }
464
+
465
+ /* Every .ejs / .html file under a directory. */
466
+ export function listViewFiles(directory: string): Array<string> {
467
+ const found: Array<string> = [];
468
+
469
+ if (!isDirectory(directory)) {
470
+ return found;
471
+ }
472
+
473
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
474
+ const fullPath: string = path.join(directory, entry.name);
475
+
476
+ if (entry.isDirectory()) {
477
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
478
+ found.push(...listViewFiles(fullPath));
479
+ }
480
+ continue;
481
+ }
482
+
483
+ if (entry.isFile() && isMarkupFile(entry.name)) {
484
+ found.push(fullPath);
485
+ }
486
+ }
487
+
488
+ return found;
489
+ }
490
+
491
+ function parseToken(token: string): ParsedToken {
492
+ const parts: Array<string> = splitVariants(token);
493
+
494
+ return {
495
+ variants: parts.slice(0, -1),
496
+ // `md:!flex` (and Tailwind 4's `md:flex!`) is still a display utility.
497
+ utility: parts[parts.length - 1]!.replace(/^!|!$/g, ""),
498
+ };
499
+ }
500
+
501
+ /* Exactly `hidden`: the only spelling the foreign `.hidden` rule matches. */
502
+ export function isBareHidden(token: string): boolean {
503
+ return token === "hidden";
504
+ }
505
+
506
+ /*
507
+ * A display utility behind at least one variant that sets something other
508
+ * than `none`: md:flex, max-lg:block, group-hover:flex, [&.open]:grid ...
509
+ */
510
+ export function isUnhidingToken(token: string): boolean {
511
+ const parsed: ParsedToken = parseToken(token);
512
+
513
+ return (
514
+ parsed.variants.length > 0 &&
515
+ parsed.utility !== "hidden" &&
516
+ DISPLAY_UTILITIES.has(parsed.utility)
517
+ );
518
+ }
519
+
520
+ /* md:hidden, max-md:hidden, group-[:not(:hover)]:hidden ... */
521
+ export function isVariantHidden(token: string): boolean {
522
+ const parsed: ParsedToken = parseToken(token);
523
+
524
+ return parsed.variants.length > 0 && parsed.utility === "hidden";
525
+ }
526
+
527
+ /*
528
+ * A `not-sr-only` behind variants that are all media queries - the ones a
529
+ * foreign `.sr-only` beats: sm:not-sr-only, max-lg:not-sr-only,
530
+ * min-[900px]:not-sr-only, print:not-sr-only, md:motion-safe:not-sr-only ...
531
+ *
532
+ * Why only those: the `.sr-only` Bootstrap 3 and HTML5 Boilerplate ship is
533
+ * not !important, so specificity decides before source order does. An @media
534
+ * wrapper adds no specificity - `.sm\:not-sr-only` inside a media query ties
535
+ * with the foreign `.sr-only` and loses to it on source order - which is what
536
+ * hid the session replay and feed labels. Every state or pseudo variant adds
537
+ * to the selector and so out-specifies that lone class wherever the foreign
538
+ * sheet sits: `.focus\:not-sr-only:focus` (the skip-link idiom `sr-only
539
+ * focus:not-sr-only`), `.hover\:x:hover`, `.group:hover .group-hover\:x`,
540
+ * `.peer:checked ~ .peer-checked\:x`, `[aria-*]`, `[data-*]`,
541
+ * `.dark\:x:is(.dark *)` under darkMode "class", and any `[&...]`. A stack
542
+ * with one such variant in it (`lg:group-hover:not-sr-only`) out-specifies it
543
+ * too. Flagging those would ask for rewrites that fix nothing.
544
+ *
545
+ * Which variants are media queries is decided by isMediaQueryVariant in
546
+ * ResponsiveVisibility.ts, beside the sr-only resolver (isVisuallyCollapsed)
547
+ * that SrOnlyForeignRule.test.tsx sweeps the tree with. That file is the
548
+ * source of truth: this check asks it rather than keeping its own list, so
549
+ * the guard and the resolver cannot drift apart.
550
+ */
551
+ export function isSrOnlyUnhidingToken(token: string): boolean {
552
+ const parsed: ParsedToken = parseToken(token);
553
+
554
+ return (
555
+ parsed.variants.length > 0 &&
556
+ parsed.utility === "not-sr-only" &&
557
+ parsed.variants.every(isMediaQueryVariant)
558
+ );
559
+ }
560
+
561
+ const HIDDEN_RULE: HidingClassRule = {
562
+ hidingClass: "hidden",
563
+ isUnhiding: isUnhidingToken,
564
+ noBreakpointAdvice: STATE_VARIANT_ADVICE,
565
+ mixedBreakpointAdvice: MIXED_BREAKPOINT_ADVICE,
566
+ disagreeingPiecesAdvice: DISAGREEING_PIECES_ADVICE,
567
+ conditionalPartnerAdvice: CONDITIONAL_PARTNER_ADVICE,
568
+ };
569
+
570
+ const SR_ONLY_RULE: HidingClassRule = {
571
+ hidingClass: "sr-only",
572
+ isUnhiding: isSrOnlyUnhidingToken,
573
+ noBreakpointAdvice: SR_ONLY_MEDIA_ADVICE,
574
+ mixedBreakpointAdvice: SR_ONLY_MIXED_BREAKPOINT_ADVICE,
575
+ disagreeingPiecesAdvice: SR_ONLY_DISAGREEING_PIECES_ADVICE,
576
+ conditionalPartnerAdvice: SR_ONLY_CONDITIONAL_PARTNER_ADVICE,
577
+ };
578
+
579
+ const HIDING_CLASS_RULES: ReadonlyArray<HidingClassRule> = [
580
+ HIDDEN_RULE,
581
+ SR_ONLY_RULE,
582
+ ];
583
+
584
+ // Every class string either rule reads spells one of these.
585
+ function mentionsHidingClass(text: string): boolean {
586
+ return text.includes("hidden") || text.includes("sr-only");
587
+ }
588
+
589
+ function carriesBare(unit: ClassTextUnit, rule: HidingClassRule): boolean {
590
+ return unit.tokens.includes(rule.hidingClass);
591
+ }
592
+
593
+ interface TokenOccurrence {
594
+ partIndex: number;
595
+ start: number;
596
+ end: number;
597
+ text: string;
598
+ }
599
+
600
+ /*
601
+ * The whole class tokens of a unit, with their positions. A token that
602
+ * touches a `${...}` is only part of a class name, so it is left out.
603
+ */
604
+ function tokenOccurrences(parts: Array<string>): Array<TokenOccurrence> {
605
+ const occurrences: Array<TokenOccurrence> = [];
606
+
607
+ parts.forEach((part: string, partIndex: number): void => {
608
+ const pattern: RegExp = /\S+/g;
609
+ let match: RegExpExecArray | null = pattern.exec(part);
610
+
611
+ while (match) {
612
+ const start: number = match.index;
613
+ const end: number = start + match[0].length;
614
+ const gluedBefore: boolean = partIndex > 0 && start === 0;
615
+ const gluedAfter: boolean =
616
+ partIndex < parts.length - 1 && end === part.length;
617
+
618
+ if (!gluedBefore && !gluedAfter) {
619
+ occurrences.push({ partIndex, start, end, text: match[0] });
620
+ }
621
+
622
+ match = pattern.exec(part);
623
+ }
624
+ });
625
+
626
+ return occurrences;
627
+ }
628
+
629
+ function makeUnit(
630
+ node: ts.Node | null,
631
+ parts: Array<string>,
632
+ substitutions: Array<string>,
633
+ ): ClassTextUnit {
634
+ return {
635
+ node,
636
+ parts,
637
+ substitutions,
638
+ tokens: tokenOccurrences(parts).map(
639
+ (occurrence: TokenOccurrence): string => {
640
+ return occurrence.text;
641
+ },
642
+ ),
643
+ };
644
+ }
645
+
646
+ function collapse(text: string): string {
647
+ return text.replace(/\s+/g, " ");
648
+ }
649
+
650
+ function renderParts(
651
+ parts: Array<string>,
652
+ substitutions: Array<string>,
653
+ ): string {
654
+ let text: string = parts[0] || "";
655
+
656
+ substitutions.forEach((substitution: string, index: number): void => {
657
+ text += `${collapse(substitution)}${parts[index + 1] || ""}`;
658
+ });
659
+
660
+ // A multi-line string reads as one line in a failure message.
661
+ return text.includes("\n") ? collapse(text).trim() : text;
662
+ }
663
+
664
+ function renderUnit(unit: ClassTextUnit): string {
665
+ return renderParts(unit.parts, unit.substitutions);
666
+ }
667
+
668
+ /* The unit with each whole token passed through `mapToken`. */
669
+ function mapTokens(
670
+ unit: ClassTextUnit,
671
+ mapToken: (token: string) => string,
672
+ ): string {
673
+ const occurrences: Array<TokenOccurrence> = tokenOccurrences(unit.parts);
674
+
675
+ const parts: Array<string> = unit.parts.map(
676
+ (part: string, partIndex: number): string => {
677
+ let rebuilt: string = "";
678
+ let cursor: number = 0;
679
+
680
+ for (const occurrence of occurrences) {
681
+ if (occurrence.partIndex !== partIndex) {
682
+ continue;
683
+ }
684
+
685
+ rebuilt += part.slice(cursor, occurrence.start);
686
+ rebuilt += mapToken(occurrence.text);
687
+ cursor = occurrence.end;
688
+ }
689
+
690
+ return rebuilt + part.slice(cursor);
691
+ },
692
+ );
693
+
694
+ return renderParts(parts, unit.substitutions);
695
+ }
696
+
697
+ /*
698
+ * The screen a lone breakpoint variant un-hides from, or null for anything
699
+ * else (a state, an arbitrary screen like min-[900px], or a stack).
700
+ */
701
+ function breakpointFixOf(token: string): BreakpointFix | null {
702
+ const parsed: ParsedToken = parseToken(token);
703
+
704
+ if (parsed.variants.length !== 1) {
705
+ return null;
706
+ }
707
+
708
+ const variant: string = parsed.variants[0]!;
709
+ const isMax: boolean = variant.startsWith("max-");
710
+ const breakpointIndex: number = BREAKPOINTS.indexOf(
711
+ isMax ? variant.slice(4) : variant,
712
+ );
713
+
714
+ if (breakpointIndex === -1) {
715
+ return null;
716
+ }
717
+
718
+ return { direction: isMax ? "max" : "min", breakpointIndex };
719
+ }
720
+
721
+ /*
722
+ * The one breakpoint rewrite for a set of tokens that always meet: min-width
723
+ * un-hiders take `max-<narrowest>:hidden`, max-width ones `<widest>:hidden`.
724
+ */
725
+ function combinedBreakpointFix(tokens: Array<string>): BreakpointFix | null {
726
+ let combined: BreakpointFix | null = null;
727
+
728
+ for (const token of tokens) {
729
+ const fix: BreakpointFix | null = breakpointFixOf(token);
730
+
731
+ if (!fix || (combined && combined.direction !== fix.direction)) {
732
+ return null;
733
+ }
734
+
735
+ if (!combined) {
736
+ combined = fix;
737
+ continue;
738
+ }
739
+
740
+ combined = {
741
+ direction: fix.direction,
742
+ breakpointIndex:
743
+ fix.direction === "min"
744
+ ? Math.min(combined.breakpointIndex, fix.breakpointIndex)
745
+ : Math.max(combined.breakpointIndex, fix.breakpointIndex),
746
+ };
747
+ }
748
+
749
+ return combined;
750
+ }
751
+
752
+ function hiddenTokenFor(fix: BreakpointFix, rule: HidingClassRule): string {
753
+ const breakpoint: string = BREAKPOINTS[fix.breakpointIndex]!;
754
+
755
+ return fix.direction === "min"
756
+ ? `max-${breakpoint}:${rule.hidingClass}`
757
+ : `${breakpoint}:${rule.hidingClass}`;
758
+ }
759
+
760
+ function breakpointRewrite(
761
+ unit: ClassTextUnit,
762
+ fix: BreakpointFix,
763
+ rule: HidingClassRule,
764
+ ): Rewrite {
765
+ const hiddenToken: string = hiddenTokenFor(fix, rule);
766
+
767
+ return {
768
+ replacement: mapTokens(unit, (token: string): string => {
769
+ return token === rule.hidingClass ? hiddenToken : token;
770
+ }),
771
+ advice: `replace the bare \`${rule.hidingClass}\` with \`${hiddenToken}\``,
772
+ };
773
+ }
774
+
775
+ function hasMixedBreakpoints(tokens: Array<string>): boolean {
776
+ return tokens.every((token: string): boolean => {
777
+ return breakpointFixOf(token) !== null;
778
+ });
779
+ }
780
+
781
+ /* The rewrite for a string that holds both the `hidden` and its un-hider. */
782
+ function sameStringRewrite(
783
+ unit: ClassTextUnit,
784
+ unhidingTokens: Array<string>,
785
+ rule: HidingClassRule,
786
+ ): Rewrite {
787
+ const fix: BreakpointFix | null = combinedBreakpointFix(unhidingTokens);
788
+
789
+ if (fix) {
790
+ return breakpointRewrite(unit, fix, rule);
791
+ }
792
+
793
+ if (hasMixedBreakpoints(unhidingTokens)) {
794
+ return { replacement: null, advice: rule.mixedBreakpointAdvice };
795
+ }
796
+
797
+ if (rule !== HIDDEN_RULE) {
798
+ return { replacement: null, advice: rule.noBreakpointAdvice };
799
+ }
800
+
801
+ /*
802
+ * `hidden ... group-hover:flex` inverts mechanically, exactly as
803
+ * FacetValueRow.tsx was converted - unless another unprefixed display
804
+ * utility would then compete with the one moved to the front.
805
+ */
806
+ const onlyToken: string | undefined =
807
+ unhidingTokens.length === 1 ? unhidingTokens[0] : undefined;
808
+ const parsed: ParsedToken | null = onlyToken ? parseToken(onlyToken) : null;
809
+ const otherUnprefixedDisplay: boolean = unit.tokens.some(
810
+ (token: string): boolean => {
811
+ return !isBareHidden(token) && DISPLAY_UTILITIES.has(token);
812
+ },
813
+ );
814
+
815
+ if (
816
+ parsed &&
817
+ parsed.variants.length === 1 &&
818
+ parsed.variants[0] === "group-hover" &&
819
+ !otherUnprefixedDisplay
820
+ ) {
821
+ return {
822
+ replacement: mapTokens(unit, (token: string): string => {
823
+ if (isBareHidden(token)) {
824
+ return parsed.utility;
825
+ }
826
+
827
+ return token === onlyToken ? "group-[:not(:hover)]:hidden" : token;
828
+ }),
829
+ advice: GROUP_HOVER_REWRITE_ADVICE,
830
+ };
831
+ }
832
+
833
+ return { replacement: null, advice: rule.noBreakpointAdvice };
834
+ }
835
+
836
+ function isExclusive(first: PlacedUnit, second: PlacedUnit): boolean {
837
+ return first.branches.some((branch: Branch): boolean => {
838
+ return second.branches.some((other: Branch): boolean => {
839
+ return other.node === branch.node && other.side !== branch.side;
840
+ });
841
+ });
842
+ }
843
+
844
+ /*
845
+ * Whether `partner` sits behind a condition `hidden` does not: a ternary
846
+ * branch, an `&&` operand or a clsx object key that the hidden piece is not
847
+ * inside as well. Then the partner can be missing while the `hidden` stays.
848
+ */
849
+ function isConditionalTo(partner: PlacedUnit, hidden: PlacedUnit): boolean {
850
+ return partner.branches.some((branch: Branch): boolean => {
851
+ return !hidden.branches.some((own: Branch): boolean => {
852
+ return own.node === branch.node && own.side === branch.side;
853
+ });
854
+ });
855
+ }
856
+
857
+ /*
858
+ * The rewrite for a `hidden` whose un-hiders sit in other pieces. Pieces that
859
+ * never meet each other (two branches of a ternary) must agree on the
860
+ * breakpoint, or no one rewrite is exact; and a partner that is only there
861
+ * under its own condition leaves nothing to rewrite the `hidden` against.
862
+ */
863
+ function crossPieceRewrite(
864
+ hidden: PlacedUnit,
865
+ partners: Array<PlacedUnit>,
866
+ rule: HidingClassRule,
867
+ ): Rewrite {
868
+ const partnerTokens: Array<Array<string>> = partners.map(
869
+ (partner: PlacedUnit): Array<string> => {
870
+ return partner.unit.tokens.filter(rule.isUnhiding);
871
+ },
872
+ );
873
+ const allTokens: Array<string> = partnerTokens.flat();
874
+
875
+ const allMeet: boolean = partners.every(
876
+ (partner: PlacedUnit, index: number): boolean => {
877
+ return partners.slice(index + 1).every((other: PlacedUnit): boolean => {
878
+ return !isExclusive(partner, other);
879
+ });
880
+ },
881
+ );
882
+
883
+ const fix: BreakpointFix | null = combinedBreakpointFix(allTokens);
884
+
885
+ if (fix && !allMeet) {
886
+ const perPartner: Array<string> = partnerTokens.map(
887
+ (tokens: Array<string>): string => {
888
+ const partnerFix: BreakpointFix | null = combinedBreakpointFix(tokens);
889
+ return partnerFix ? hiddenTokenFor(partnerFix, rule) : "";
890
+ },
891
+ );
892
+
893
+ if (new Set<string>(perPartner).size !== 1) {
894
+ return { replacement: null, advice: rule.disagreeingPiecesAdvice };
895
+ }
896
+ }
897
+
898
+ if (
899
+ fix &&
900
+ partners.some((partner: PlacedUnit): boolean => {
901
+ return isConditionalTo(partner, hidden);
902
+ })
903
+ ) {
904
+ return { replacement: null, advice: rule.conditionalPartnerAdvice };
905
+ }
906
+
907
+ if (fix) {
908
+ return breakpointRewrite(hidden.unit, fix, rule);
909
+ }
910
+
911
+ if (hasMixedBreakpoints(allTokens)) {
912
+ return { replacement: null, advice: rule.mixedBreakpointAdvice };
913
+ }
914
+
915
+ return { replacement: null, advice: rule.noBreakpointAdvice };
916
+ }
917
+
918
+ function nameText(name: ts.Node | undefined): string | null {
919
+ if (!name) {
920
+ return null;
921
+ }
922
+
923
+ if (
924
+ ts.isIdentifier(name) ||
925
+ ts.isPrivateIdentifier(name) ||
926
+ ts.isStringLiteral(name) ||
927
+ ts.isNoSubstitutionTemplateLiteral(name)
928
+ ) {
929
+ return name.text;
930
+ }
931
+
932
+ return null;
933
+ }
934
+
935
+ function isClassName(name: ts.Node | undefined): boolean {
936
+ const text: string | null = nameText(name);
937
+ return text !== null && CLASS_NAME_PATTERN.test(text);
938
+ }
939
+
940
+ function calleeName(expression: ts.Expression): string | null {
941
+ if (ts.isIdentifier(expression)) {
942
+ return expression.text;
943
+ }
944
+
945
+ if (ts.isPropertyAccessExpression(expression)) {
946
+ return expression.name.text;
947
+ }
948
+
949
+ return null;
950
+ }
951
+
952
+ function templateUnit(
953
+ node: ts.TemplateExpression,
954
+ sourceFile: ts.SourceFile,
955
+ ): ClassTextUnit {
956
+ return makeUnit(
957
+ node,
958
+ [
959
+ node.head.text,
960
+ ...node.templateSpans.map((span: ts.TemplateSpan): string => {
961
+ return span.literal.text;
962
+ }),
963
+ ],
964
+ node.templateSpans.map((span: ts.TemplateSpan): string => {
965
+ return `\${${span.expression.getText(sourceFile)}}`;
966
+ }),
967
+ );
968
+ }
969
+
970
+ /* The class text a node is on its own, or null when it is not a string. */
971
+ function unitOf(
972
+ node: ts.Node,
973
+ sourceFile: ts.SourceFile,
974
+ ): ClassTextUnit | null {
975
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
976
+ return makeUnit(node, [node.text], []);
977
+ }
978
+
979
+ if (ts.isTemplateExpression(node)) {
980
+ return templateUnit(node, sourceFile);
981
+ }
982
+
983
+ return null;
984
+ }
985
+
986
+ /* Return statements of a function body, not of functions nested inside it. */
987
+ function collectReturns(node: ts.Node, into: Array<ts.ReturnStatement>): void {
988
+ if (ts.isReturnStatement(node)) {
989
+ into.push(node);
990
+ return;
991
+ }
992
+
993
+ if (ts.isFunctionLike(node)) {
994
+ return;
995
+ }
996
+
997
+ ts.forEachChild(node, (child: ts.Node): void => {
998
+ collectReturns(child, into);
999
+ });
1000
+ }
1001
+
1002
+ /*
1003
+ * Every piece of class text that can end up in the value of `node`, with the
1004
+ * branches that decide whether it does. Anything that is not a value - a
1005
+ * condition, a comparison, an argument to an unknown function, an object
1006
+ * literal, JSX - is not followed.
1007
+ */
1008
+ function collectValueUnits(
1009
+ node: ts.Node,
1010
+ branches: Array<Branch>,
1011
+ sourceFile: ts.SourceFile,
1012
+ into: Array<PlacedUnit>,
1013
+ ): void {
1014
+ const unit: ClassTextUnit | null = unitOf(node, sourceFile);
1015
+
1016
+ if (unit) {
1017
+ into.push({ unit, branches });
1018
+
1019
+ if (ts.isTemplateExpression(node)) {
1020
+ for (const span of node.templateSpans) {
1021
+ collectValueUnits(span.expression, branches, sourceFile, into);
1022
+ }
1023
+ }
1024
+
1025
+ return;
1026
+ }
1027
+
1028
+ if (
1029
+ ts.isParenthesizedExpression(node) ||
1030
+ ts.isAsExpression(node) ||
1031
+ ts.isNonNullExpression(node) ||
1032
+ ts.isSatisfiesExpression(node) ||
1033
+ ts.isTypeAssertionExpression(node) ||
1034
+ ts.isSpreadElement(node)
1035
+ ) {
1036
+ collectValueUnits(node.expression, branches, sourceFile, into);
1037
+ return;
1038
+ }
1039
+
1040
+ if (ts.isConditionalExpression(node)) {
1041
+ collectValueUnits(
1042
+ node.whenTrue,
1043
+ [...branches, { node, side: 0 }],
1044
+ sourceFile,
1045
+ into,
1046
+ );
1047
+ collectValueUnits(
1048
+ node.whenFalse,
1049
+ [...branches, { node, side: 1 }],
1050
+ sourceFile,
1051
+ into,
1052
+ );
1053
+ return;
1054
+ }
1055
+
1056
+ if (ts.isBinaryExpression(node)) {
1057
+ const operator: ts.SyntaxKind = node.operatorToken.kind;
1058
+
1059
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) {
1060
+ // One-sided: only there when the left operand holds.
1061
+ collectValueUnits(
1062
+ node.right,
1063
+ [...branches, { node, side: 0 }],
1064
+ sourceFile,
1065
+ into,
1066
+ );
1067
+ } else if (
1068
+ operator === ts.SyntaxKind.BarBarToken ||
1069
+ operator === ts.SyntaxKind.QuestionQuestionToken
1070
+ ) {
1071
+ collectValueUnits(
1072
+ node.left,
1073
+ [...branches, { node, side: 0 }],
1074
+ sourceFile,
1075
+ into,
1076
+ );
1077
+ collectValueUnits(
1078
+ node.right,
1079
+ [...branches, { node, side: 1 }],
1080
+ sourceFile,
1081
+ into,
1082
+ );
1083
+ } else if (operator === ts.SyntaxKind.PlusToken) {
1084
+ collectValueUnits(node.left, branches, sourceFile, into);
1085
+ collectValueUnits(node.right, branches, sourceFile, into);
1086
+ }
1087
+ return;
1088
+ }
1089
+
1090
+ if (ts.isArrayLiteralExpression(node)) {
1091
+ for (const element of node.elements) {
1092
+ collectValueUnits(element, branches, sourceFile, into);
1093
+ }
1094
+ return;
1095
+ }
1096
+
1097
+ if (ts.isCallExpression(node)) {
1098
+ const name: string | null = calleeName(node.expression);
1099
+
1100
+ if (name && CLASS_HELPERS.has(name)) {
1101
+ for (const argument of node.arguments) {
1102
+ if (ts.isObjectLiteralExpression(argument)) {
1103
+ // clsx({ "md:flex": wide, hidden: !open }): the keys are the classes.
1104
+ for (const property of argument.properties) {
1105
+ const key: string | null = nameText(property.name);
1106
+
1107
+ if (property.name && key !== null) {
1108
+ into.push({
1109
+ unit: makeUnit(property.name, [key], []),
1110
+ // Each key is only there when its value holds.
1111
+ branches: [...branches, { node: property, side: 0 }],
1112
+ });
1113
+ }
1114
+ }
1115
+ continue;
1116
+ }
1117
+
1118
+ collectValueUnits(argument, branches, sourceFile, into);
1119
+ }
1120
+ return;
1121
+ }
1122
+
1123
+ if (
1124
+ name &&
1125
+ PASS_THROUGH_METHODS.has(name) &&
1126
+ ts.isPropertyAccessExpression(node.expression)
1127
+ ) {
1128
+ collectValueUnits(node.expression.expression, branches, sourceFile, into);
1129
+
1130
+ if (name === "concat") {
1131
+ for (const argument of node.arguments) {
1132
+ collectValueUnits(argument, branches, sourceFile, into);
1133
+ }
1134
+ }
1135
+ }
1136
+ return;
1137
+ }
1138
+
1139
+ // NavLink-style `className={({ isActive }) => ...}` and class getters.
1140
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1141
+ if (!ts.isBlock(node.body)) {
1142
+ collectValueUnits(node.body, branches, sourceFile, into);
1143
+ return;
1144
+ }
1145
+
1146
+ const returns: Array<ts.ReturnStatement> = [];
1147
+ collectReturns(node.body, returns);
1148
+
1149
+ returns.forEach((statement: ts.ReturnStatement, index: number): void => {
1150
+ if (statement.expression) {
1151
+ collectValueUnits(
1152
+ statement.expression,
1153
+ [...branches, { node, side: index }],
1154
+ sourceFile,
1155
+ into,
1156
+ );
1157
+ }
1158
+ });
1159
+ }
1160
+ }
1161
+
1162
+ /* The expression whose value is a class list, when `node` introduces one. */
1163
+ function classExpressionOf(
1164
+ node: ts.Node,
1165
+ sourceFile: ts.SourceFile,
1166
+ ): ts.Node | null {
1167
+ if (ts.isJsxAttribute(node)) {
1168
+ if (
1169
+ !CLASS_NAME_PATTERN.test(node.name.getText(sourceFile)) ||
1170
+ !node.initializer
1171
+ ) {
1172
+ return null;
1173
+ }
1174
+
1175
+ if (ts.isJsxExpression(node.initializer)) {
1176
+ return node.initializer.expression || null;
1177
+ }
1178
+
1179
+ return node.initializer;
1180
+ }
1181
+
1182
+ if (
1183
+ (ts.isVariableDeclaration(node) ||
1184
+ ts.isPropertyAssignment(node) ||
1185
+ ts.isPropertyDeclaration(node) ||
1186
+ ts.isParameter(node) ||
1187
+ ts.isBindingElement(node)) &&
1188
+ node.initializer &&
1189
+ isClassName(node.name)
1190
+ ) {
1191
+ return node.initializer;
1192
+ }
1193
+
1194
+ if (
1195
+ ts.isBinaryExpression(node) &&
1196
+ (node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
1197
+ node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken ||
1198
+ node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ||
1199
+ node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken)
1200
+ ) {
1201
+ const target: ts.Node | undefined = ts.isPropertyAccessExpression(node.left)
1202
+ ? node.left.name
1203
+ : node.left;
1204
+
1205
+ return isClassName(target) ? node.right : null;
1206
+ }
1207
+
1208
+ if (ts.isCallExpression(node)) {
1209
+ const name: string | null = calleeName(node.expression);
1210
+ return name && CLASS_HELPERS.has(name) ? node : null;
1211
+ }
1212
+
1213
+ // The substitutions of a template always meet its static text.
1214
+ if (ts.isTemplateExpression(node)) {
1215
+ return node;
1216
+ }
1217
+
1218
+ return null;
1219
+ }
1220
+
1221
+ function sameStringFinding(
1222
+ file: string,
1223
+ line: number,
1224
+ unit: ClassTextUnit,
1225
+ rule: HidingClassRule,
1226
+ ): ForeignHiddenRuleFinding | null {
1227
+ const unhidingTokens: Array<string> = unit.tokens.filter(rule.isUnhiding);
1228
+
1229
+ if (!carriesBare(unit, rule) || unhidingTokens.length === 0) {
1230
+ return null;
1231
+ }
1232
+
1233
+ const rewrite: Rewrite = sameStringRewrite(unit, unhidingTokens, rule);
1234
+
1235
+ return {
1236
+ file,
1237
+ line,
1238
+ kind: "same-string",
1239
+ hidingClass: rule.hidingClass,
1240
+ text: renderUnit(unit),
1241
+ partnerText: null,
1242
+ unhidingTokens,
1243
+ replacement: rewrite.replacement,
1244
+ advice: rewrite.advice,
1245
+ };
1246
+ }
1247
+
1248
+ /* The other pieces that meet `hidden` and show it again. */
1249
+ function partnersOf(
1250
+ hidden: PlacedUnit,
1251
+ placed: Array<PlacedUnit>,
1252
+ rule: HidingClassRule,
1253
+ ): Array<PlacedUnit> {
1254
+ return placed.filter((other: PlacedUnit): boolean => {
1255
+ return (
1256
+ other.unit !== hidden.unit &&
1257
+ !isExclusive(hidden, other) &&
1258
+ other.unit.tokens.some(rule.isUnhiding)
1259
+ );
1260
+ });
1261
+ }
1262
+
1263
+ function crossPieceFinding(
1264
+ file: string,
1265
+ line: number,
1266
+ hidden: PlacedUnit,
1267
+ partners: Array<PlacedUnit>,
1268
+ rule: HidingClassRule,
1269
+ ): ForeignHiddenRuleFinding {
1270
+ const rewrite: Rewrite = crossPieceRewrite(hidden, partners, rule);
1271
+
1272
+ return {
1273
+ file,
1274
+ line,
1275
+ kind: "cross-piece",
1276
+ hidingClass: rule.hidingClass,
1277
+ text: renderUnit(hidden.unit),
1278
+ partnerText: renderUnit(partners[0]!.unit),
1279
+ unhidingTokens: partners.flatMap((partner: PlacedUnit): Array<string> => {
1280
+ return partner.unit.tokens.filter(rule.isUnhiding);
1281
+ }),
1282
+ replacement: rewrite.replacement,
1283
+ advice: rewrite.advice,
1284
+ };
1285
+ }
1286
+
1287
+ function sortByLine(findings: Array<ForeignHiddenRuleFinding>): void {
1288
+ findings.sort(
1289
+ (first: ForeignHiddenRuleFinding, second: ForeignHiddenRuleFinding) => {
1290
+ return first.line - second.line;
1291
+ },
1292
+ );
1293
+ }
1294
+
1295
+ interface TreeAnalysis extends SourceAnalysis {
1296
+ // Per hiding class: every string node already reported, on its own or as the hidden piece.
1297
+ reported: Map<HidingClass, Set<ts.Node>>;
1298
+ }
1299
+
1300
+ /*
1301
+ * Reads one parsed module (or the code of one EJS tag). `lineOffset` maps the
1302
+ * tree's lines onto the file's.
1303
+ */
1304
+ function analyzeTree(
1305
+ fileName: string,
1306
+ sourceFile: ts.SourceFile,
1307
+ lineOffset: number,
1308
+ ): TreeAnalysis {
1309
+ const analysis: TreeAnalysis = {
1310
+ findings: [],
1311
+ variantHiddenSites: [],
1312
+ reported: new Map<HidingClass, Set<ts.Node>>(),
1313
+ };
1314
+
1315
+ const lineOf: (node: ts.Node) => number = (node: ts.Node): number => {
1316
+ return (
1317
+ sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line +
1318
+ 1 +
1319
+ lineOffset
1320
+ );
1321
+ };
1322
+
1323
+ const units: Array<ClassTextUnit> = [];
1324
+ const classExpressions: Array<ts.Node> = [];
1325
+
1326
+ const visit: (node: ts.Node) => void = (node: ts.Node): void => {
1327
+ const unit: ClassTextUnit | null = unitOf(node, sourceFile);
1328
+
1329
+ if (unit) {
1330
+ units.push(unit);
1331
+
1332
+ if (unit.tokens.some(isVariantHidden)) {
1333
+ analysis.variantHiddenSites.push({
1334
+ file: fileName,
1335
+ line: lineOf(node),
1336
+ text: renderUnit(unit),
1337
+ });
1338
+ }
1339
+ }
1340
+
1341
+ const classExpression: ts.Node | null = classExpressionOf(node, sourceFile);
1342
+
1343
+ if (classExpression) {
1344
+ classExpressions.push(classExpression);
1345
+ }
1346
+
1347
+ ts.forEachChild(node, visit);
1348
+ };
1349
+
1350
+ visit(sourceFile);
1351
+
1352
+ for (const rule of HIDING_CLASS_RULES) {
1353
+ // One finding per bare class, however many class expressions hold it.
1354
+ const reported: Set<ts.Node> = new Set<ts.Node>();
1355
+ analysis.reported.set(rule.hidingClass, reported);
1356
+
1357
+ for (const unit of units) {
1358
+ const finding: ForeignHiddenRuleFinding | null = sameStringFinding(
1359
+ fileName,
1360
+ lineOf(unit.node!),
1361
+ unit,
1362
+ rule,
1363
+ );
1364
+
1365
+ if (finding) {
1366
+ reported.add(unit.node!);
1367
+ analysis.findings.push(finding);
1368
+ }
1369
+ }
1370
+
1371
+ for (const expression of classExpressions) {
1372
+ const placed: Array<PlacedUnit> = [];
1373
+ collectValueUnits(expression, [], sourceFile, placed);
1374
+
1375
+ for (const hidden of placed) {
1376
+ const node: ts.Node | null = hidden.unit.node;
1377
+
1378
+ if (!node || !carriesBare(hidden.unit, rule) || reported.has(node)) {
1379
+ continue;
1380
+ }
1381
+
1382
+ const partners: Array<PlacedUnit> = partnersOf(hidden, placed, rule);
1383
+
1384
+ if (partners.length === 0) {
1385
+ continue;
1386
+ }
1387
+
1388
+ reported.add(node);
1389
+ analysis.findings.push(
1390
+ crossPieceFinding(fileName, lineOf(node), hidden, partners, rule),
1391
+ );
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ sortByLine(analysis.findings);
1397
+
1398
+ return analysis;
1399
+ }
1400
+
1401
+ /*
1402
+ * Reads one module. Parsed rather than grepped, so a comment, a JSDoc example
1403
+ * or JSX text that quotes `hidden md:flex` is never a finding.
1404
+ */
1405
+ export function analyzeSource(
1406
+ fileName: string,
1407
+ source: string,
1408
+ ): SourceAnalysis {
1409
+ // Every class string this guard reads spells "hidden" or "sr-only" somewhere.
1410
+ if (!mentionsHidingClass(source)) {
1411
+ return { findings: [], variantHiddenSites: [] };
1412
+ }
1413
+
1414
+ const sourceFile: ts.SourceFile = ts.createSourceFile(
1415
+ fileName,
1416
+ source,
1417
+ ts.ScriptTarget.Latest,
1418
+ true,
1419
+ fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
1420
+ );
1421
+
1422
+ const tree: TreeAnalysis = analyzeTree(fileName, sourceFile, 0);
1423
+
1424
+ return {
1425
+ findings: tree.findings,
1426
+ variantHiddenSites: tree.variantHiddenSites,
1427
+ };
1428
+ }
1429
+
1430
+ /*
1431
+ * ---------------------------------------------------------------------------
1432
+ * Server-rendered views: EJS templates and plain HTML.
1433
+ *
1434
+ * The Docs, the API Reference and the website load the same Play CDN and used
1435
+ * the same `hidden <bp>:<display>` markup - under a foreign `.hidden` rule the
1436
+ * Docs sidebar and search, the API Reference rail and the website's primary
1437
+ * nav all disappeared on desktop. A view is not TypeScript, so it is read in
1438
+ * three layers:
1439
+ *
1440
+ * - Markup. Every class attribute of every start tag, double- or
1441
+ * single-quoted, over as many lines as it takes. Each `<% %>` tag inside
1442
+ * the value is dropped and treated as a token boundary, exactly like a
1443
+ * `${...}` substitution: a token glued to one (`bg-<%= tone %>-50`) is
1444
+ * only part of a class name and is not read. Other attributes
1445
+ * (data-class, :class, className=) are not class attributes. HTML
1446
+ * comments, <style>, <textarea> and <title> hold no markup and are
1447
+ * skipped; so is markup shown as escaped text (`&lt;div class=...`),
1448
+ * which is not a tag.
1449
+ * - Script text. A <script> element's content is JavaScript, not markup,
1450
+ * but markup written inside its strings (`el.innerHTML = '<span
1451
+ * class="...">'`) becomes a real class attribute the moment it is
1452
+ * inserted. So `class="..."` inside a <script> is read as an attribute
1453
+ * too. Class names a script sets through `className =` or `classList`
1454
+ * are not attributes and are not read here: a lone `hidden` toggled from
1455
+ * JavaScript is the one use the foreign rule cannot break.
1456
+ * - Code. The JavaScript inside every `<% %>` tag - anywhere in the file,
1457
+ * not only in class attributes - goes through the same TypeScript reader
1458
+ * as a module (analyzeTree), so `<% const navClass = 'hidden md:flex' %>`
1459
+ * is caught where it is written.
1460
+ *
1461
+ * And across markup and code: a class attribute's static text and the strings its
1462
+ * output tags (`<%= %>`, `<%- %>`) can print always meet in one class value,
1463
+ * so `class="md:flex <%= open ? '' : 'hidden' %>"` is the cross-piece shape
1464
+ * with the pieces on either side of a `<% %>`. Only a tag's value is class
1465
+ * text: a condition (`mode === 'hidden' ? ...`) is not, and `<% %>`
1466
+ * scriptlets print nothing.
1467
+ * ---------------------------------------------------------------------------
1468
+ */
1469
+
1470
+ export interface EjsTag {
1471
+ // Offsets of the whole tag in the file, `<%` to `%>` (end exclusive).
1472
+ start: number;
1473
+ end: number;
1474
+ // As written, delimiters included.
1475
+ text: string;
1476
+ // The JavaScript between the delimiters.
1477
+ code: string;
1478
+ // `<%=` and `<%-` print their value; `<%`, `<%_` and `<%#` print nothing.
1479
+ outputs: boolean;
1480
+ isComment: boolean;
1481
+ // The line the code starts on.
1482
+ codeLine: number;
1483
+ }
1484
+
1485
+ export interface ClassAttribute {
1486
+ // The line the attribute's name is on.
1487
+ line: number;
1488
+ // The static text of the value, split at each tag: one more entry than `tags`.
1489
+ parts: Array<string>;
1490
+ tags: Array<EjsTag>;
1491
+ }
1492
+
1493
+ // Stands in for tag text while the markup is read, so nothing inside a tag - a quote, a `>` - is markup.
1494
+ const TAG_PLACEHOLDER: string = "\u0000";
1495
+
1496
+ // Elements whose content is text, not markup.
1497
+ const TEXT_ONLY_ELEMENTS: ReadonlySet<string> = new Set<string>([
1498
+ "style",
1499
+ "textarea",
1500
+ "title",
1501
+ ]);
1502
+
1503
+ // `class="` / `class='` inside script text, allowing `class=\"...\"` in a double-quoted JS string.
1504
+ const SCRIPT_CLASS_ATTRIBUTE: RegExp = /(^|[\s"'`])class\s*=\s*(\\?)(["'])/gi;
1505
+
1506
+ // The HTML tokenizer's character classes (the tag text is masked out first).
1507
+ const WHITESPACE: RegExp = /\s/;
1508
+ const TAG_NAME_START: RegExp = /[A-Za-z]/;
1509
+ const TAG_NAME_END: RegExp = /[\s/>]/;
1510
+ const ATTRIBUTE_SEPARATOR: RegExp = /[\s/]/;
1511
+ const ATTRIBUTE_NAME_END: RegExp = /[\s/>=]/;
1512
+ const UNQUOTED_VALUE_END: RegExp = /[\s>]/;
1513
+
1514
+ /* Offsets where each line starts, for turning an offset into a line. */
1515
+ function lineStartsOf(source: string): Array<number> {
1516
+ const starts: Array<number> = [0];
1517
+
1518
+ for (let index: number = 0; index < source.length; index++) {
1519
+ if (source[index] === "\n") {
1520
+ starts.push(index + 1);
1521
+ }
1522
+ }
1523
+
1524
+ return starts;
1525
+ }
1526
+
1527
+ function lineAt(lineStarts: Array<number>, offset: number): number {
1528
+ let low: number = 0;
1529
+ let high: number = lineStarts.length - 1;
1530
+
1531
+ while (low < high) {
1532
+ const middle: number = Math.ceil((low + high) / 2);
1533
+
1534
+ if (lineStarts[middle]! <= offset) {
1535
+ low = middle;
1536
+ } else {
1537
+ high = middle - 1;
1538
+ }
1539
+ }
1540
+
1541
+ return low + 1;
1542
+ }
1543
+
1544
+ function findEjsTagsWithLines(
1545
+ source: string,
1546
+ lineStarts: Array<number>,
1547
+ ): Array<EjsTag> {
1548
+ const tags: Array<EjsTag> = [];
1549
+ let index: number = 0;
1550
+
1551
+ for (;;) {
1552
+ const start: number = source.indexOf("<%", index);
1553
+
1554
+ if (start === -1) {
1555
+ return tags;
1556
+ }
1557
+
1558
+ const marker: string = source[start + 2] || "";
1559
+
1560
+ // `<%%` prints a literal `<%`: static text, not a tag.
1561
+ if (marker === "%") {
1562
+ index = start + 3;
1563
+ continue;
1564
+ }
1565
+
1566
+ const close: number = source.indexOf("%>", start + 2);
1567
+ const end: number = close === -1 ? source.length : close + 2;
1568
+ const codeStart: number = start + 2 + ("=-_#".includes(marker) ? 1 : 0);
1569
+ let codeEnd: number = close === -1 ? source.length : close;
1570
+
1571
+ // `-%>` and `_%>` trim whitespace after the tag; the `-` / `_` is not code.
1572
+ if (
1573
+ codeEnd > codeStart &&
1574
+ (source[codeEnd - 1] === "-" || source[codeEnd - 1] === "_")
1575
+ ) {
1576
+ codeEnd--;
1577
+ }
1578
+
1579
+ tags.push({
1580
+ start,
1581
+ end,
1582
+ text: source.slice(start, end),
1583
+ code: source.slice(codeStart, codeEnd),
1584
+ outputs: marker === "=" || marker === "-",
1585
+ isComment: marker === "#",
1586
+ codeLine: lineAt(lineStarts, codeStart),
1587
+ });
1588
+
1589
+ index = end;
1590
+ }
1591
+ }
1592
+
1593
+ /* Every `<% %>` tag in an EJS template, in order. */
1594
+ export function findEjsTags(source: string): Array<EjsTag> {
1595
+ return findEjsTagsWithLines(source, lineStartsOf(source));
1596
+ }
1597
+
1598
+ function maskTags(source: string, tags: Array<EjsTag>): string {
1599
+ let masked: string = "";
1600
+ let cursor: number = 0;
1601
+
1602
+ for (const tag of tags) {
1603
+ masked += source.slice(cursor, tag.start);
1604
+ /*
1605
+ * Same length, so an offset means the same in both strings (lines are
1606
+ * read from the source). Newlines go too: an unquoted value ends at
1607
+ * whitespace, and must not end inside a tag.
1608
+ */
1609
+ masked += TAG_PLACEHOLDER.repeat(tag.text.length);
1610
+ cursor = tag.end;
1611
+ }
1612
+
1613
+ return masked + source.slice(cursor);
1614
+ }
1615
+
1616
+ interface AttributeRange {
1617
+ nameStart: number;
1618
+ valueStart: number;
1619
+ valueEnd: number;
1620
+ }
1621
+
1622
+ function isSpace(character: string | undefined): boolean {
1623
+ return character !== undefined && WHITESPACE.test(character);
1624
+ }
1625
+
1626
+ /*
1627
+ * Reads one start tag from just after its `<`, recording its class
1628
+ * attributes. Returns the tag's name and the offset after its `>`.
1629
+ */
1630
+ function readStartTag(
1631
+ masked: string,
1632
+ from: number,
1633
+ into: Array<AttributeRange>,
1634
+ ): { name: string; end: number } {
1635
+ const length: number = masked.length;
1636
+ let cursor: number = from;
1637
+
1638
+ while (cursor < length && !TAG_NAME_END.test(masked[cursor]!)) {
1639
+ cursor++;
1640
+ }
1641
+
1642
+ const name: string = masked.slice(from, cursor).toLowerCase();
1643
+
1644
+ for (;;) {
1645
+ while (cursor < length && ATTRIBUTE_SEPARATOR.test(masked[cursor]!)) {
1646
+ cursor++;
1647
+ }
1648
+
1649
+ if (cursor >= length) {
1650
+ return { name, end: length };
1651
+ }
1652
+
1653
+ if (masked[cursor] === ">") {
1654
+ return { name, end: cursor + 1 };
1655
+ }
1656
+
1657
+ const nameStart: number = cursor;
1658
+ cursor++;
1659
+
1660
+ while (cursor < length && !ATTRIBUTE_NAME_END.test(masked[cursor]!)) {
1661
+ cursor++;
1662
+ }
1663
+
1664
+ const attributeName: string = masked.slice(nameStart, cursor);
1665
+ let after: number = cursor;
1666
+
1667
+ while (isSpace(masked[after])) {
1668
+ after++;
1669
+ }
1670
+
1671
+ // An attribute with no value (`disabled`, `hidden`).
1672
+ if (masked[after] !== "=") {
1673
+ continue;
1674
+ }
1675
+
1676
+ after++;
1677
+
1678
+ while (isSpace(masked[after])) {
1679
+ after++;
1680
+ }
1681
+
1682
+ const quote: string | undefined = masked[after];
1683
+ let valueStart: number;
1684
+ let valueEnd: number;
1685
+
1686
+ if (quote === '"' || quote === "'") {
1687
+ valueStart = after + 1;
1688
+ const close: number = masked.indexOf(quote, valueStart);
1689
+ valueEnd = close === -1 ? length : close;
1690
+ cursor = close === -1 ? length : close + 1;
1691
+ } else {
1692
+ valueStart = after;
1693
+ cursor = after;
1694
+
1695
+ while (cursor < length && !UNQUOTED_VALUE_END.test(masked[cursor]!)) {
1696
+ cursor++;
1697
+ }
1698
+
1699
+ valueEnd = cursor;
1700
+ }
1701
+
1702
+ if (attributeName.toLowerCase() === "class") {
1703
+ into.push({ nameStart, valueStart, valueEnd });
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ /* `class="..."` written inside script text, e.g. in an innerHTML string. */
1709
+ function readScriptText(
1710
+ masked: string,
1711
+ start: number,
1712
+ end: number,
1713
+ into: Array<AttributeRange>,
1714
+ ): void {
1715
+ const text: string = masked.slice(start, end);
1716
+ const pattern: RegExp = new RegExp(SCRIPT_CLASS_ATTRIBUTE.source, "gi");
1717
+ let match: RegExpExecArray | null = pattern.exec(text);
1718
+
1719
+ while (match) {
1720
+ const closing: string = `${match[2]}${match[3]}`;
1721
+ const valueStart: number = match.index + match[0].length;
1722
+ const close: number = text.indexOf(closing, valueStart);
1723
+ const valueEnd: number = close === -1 ? text.length : close;
1724
+
1725
+ into.push({
1726
+ nameStart: start + match.index + match[1]!.length,
1727
+ valueStart: start + valueStart,
1728
+ valueEnd: start + valueEnd,
1729
+ });
1730
+
1731
+ pattern.lastIndex = close === -1 ? text.length : close + closing.length;
1732
+ match = pattern.exec(text);
1733
+ }
1734
+ }
1735
+
1736
+ function closingTagOffset(masked: string, name: string, from: number): number {
1737
+ const pattern: RegExp = new RegExp(`</${name}(?=[\\s/>]|$)`, "gi");
1738
+ pattern.lastIndex = from;
1739
+ const match: RegExpExecArray | null = pattern.exec(masked);
1740
+
1741
+ return match ? match.index : masked.length;
1742
+ }
1743
+
1744
+ /* Every class attribute's value range, read from the tag-masked markup. */
1745
+ function classAttributeRanges(masked: string): Array<AttributeRange> {
1746
+ const ranges: Array<AttributeRange> = [];
1747
+ let index: number = 0;
1748
+
1749
+ while (index < masked.length) {
1750
+ const open: number = masked.indexOf("<", index);
1751
+
1752
+ if (open === -1) {
1753
+ break;
1754
+ }
1755
+
1756
+ if (masked.startsWith("<!--", open)) {
1757
+ const close: number = masked.indexOf("-->", open + 4);
1758
+ index = close === -1 ? masked.length : close + 3;
1759
+ continue;
1760
+ }
1761
+
1762
+ // A letter starts a tag name; so does a tag, as in `<<%= headingTag %> class="...">`.
1763
+ const next: string = masked[open + 1] || "";
1764
+
1765
+ if (!TAG_NAME_START.test(next) && next !== TAG_PLACEHOLDER) {
1766
+ index = open + 1;
1767
+ continue;
1768
+ }
1769
+
1770
+ const tag: { name: string; end: number } = readStartTag(
1771
+ masked,
1772
+ open + 1,
1773
+ ranges,
1774
+ );
1775
+ index = tag.end;
1776
+
1777
+ if (tag.name === "script" || TEXT_ONLY_ELEMENTS.has(tag.name)) {
1778
+ const contentEnd: number = closingTagOffset(masked, tag.name, index);
1779
+
1780
+ if (tag.name === "script") {
1781
+ readScriptText(masked, index, contentEnd, ranges);
1782
+ }
1783
+
1784
+ index = contentEnd;
1785
+ }
1786
+ }
1787
+
1788
+ return ranges;
1789
+ }
1790
+
1791
+ interface MarkupReading {
1792
+ lineStarts: Array<number>;
1793
+ tags: Array<EjsTag>;
1794
+ attributes: Array<ClassAttribute>;
1795
+ }
1796
+
1797
+ function readMarkup(source: string): MarkupReading {
1798
+ const lineStarts: Array<number> = lineStartsOf(source);
1799
+ const tags: Array<EjsTag> = findEjsTagsWithLines(source, lineStarts);
1800
+ const masked: string = maskTags(source, tags);
1801
+ let tagIndex: number = 0;
1802
+
1803
+ // Ranges come in document order and never overlap, so one pass over the tags does.
1804
+ const attributes: Array<ClassAttribute> = classAttributeRanges(masked).map(
1805
+ (range: AttributeRange): ClassAttribute => {
1806
+ while (
1807
+ tagIndex < tags.length &&
1808
+ tags[tagIndex]!.start < range.valueStart
1809
+ ) {
1810
+ tagIndex++;
1811
+ }
1812
+
1813
+ const inside: Array<EjsTag> = [];
1814
+ const parts: Array<string> = [];
1815
+ let cursor: number = range.valueStart;
1816
+
1817
+ while (tagIndex < tags.length && tags[tagIndex]!.end <= range.valueEnd) {
1818
+ const tag: EjsTag = tags[tagIndex]!;
1819
+ parts.push(source.slice(cursor, tag.start));
1820
+ inside.push(tag);
1821
+ cursor = tag.end;
1822
+ tagIndex++;
1823
+ }
1824
+
1825
+ parts.push(source.slice(cursor, range.valueEnd));
1826
+
1827
+ return {
1828
+ line: lineAt(lineStarts, range.nameStart),
1829
+ parts,
1830
+ tags: inside,
1831
+ };
1832
+ },
1833
+ );
1834
+
1835
+ return { lineStarts, tags, attributes };
1836
+ }
1837
+
1838
+ /* The class attributes of an EJS template or HTML page (see the notes above). */
1839
+ export function extractClassAttributes(source: string): Array<ClassAttribute> {
1840
+ return readMarkup(source).attributes;
1841
+ }
1842
+
1843
+ interface ParsedTag {
1844
+ sourceFile: ts.SourceFile;
1845
+ // An output tag's value; null for a scriptlet or a comment.
1846
+ expression: ts.Expression | null;
1847
+ tree: TreeAnalysis | null;
1848
+ }
1849
+
1850
+ function parseTag(fileName: string, tag: EjsTag): ParsedTag {
1851
+ // Parenthesised, an output tag is one expression: `{ a: 1 }` is an object, not a block.
1852
+ const code: string = tag.outputs ? `(${tag.code}\n)` : tag.code;
1853
+ const sourceFile: ts.SourceFile = ts.createSourceFile(
1854
+ `${fileName}:${tag.codeLine}.ts`,
1855
+ code,
1856
+ ts.ScriptTarget.Latest,
1857
+ true,
1858
+ ts.ScriptKind.TS,
1859
+ );
1860
+
1861
+ const first: ts.Statement | undefined = sourceFile.statements[0];
1862
+
1863
+ return {
1864
+ sourceFile,
1865
+ expression:
1866
+ tag.outputs && first && ts.isExpressionStatement(first)
1867
+ ? first.expression
1868
+ : null,
1869
+ tree: mentionsHidingClass(tag.code)
1870
+ ? analyzeTree(fileName, sourceFile, tag.codeLine - 1)
1871
+ : null,
1872
+ };
1873
+ }
1874
+
1875
+ /*
1876
+ * Reads one EJS template or HTML page: the markup, the code in its tags, and
1877
+ * the class attributes whose static text and printed values meet.
1878
+ */
1879
+ export function analyzeMarkup(
1880
+ fileName: string,
1881
+ source: string,
1882
+ ): SourceAnalysis {
1883
+ const analysis: SourceAnalysis = { findings: [], variantHiddenSites: [] };
1884
+
1885
+ if (!mentionsHidingClass(source)) {
1886
+ return analysis;
1887
+ }
1888
+
1889
+ const reading: MarkupReading = readMarkup(source);
1890
+ const parsedTags: Map<EjsTag, ParsedTag> = new Map<EjsTag, ParsedTag>();
1891
+
1892
+ const parsed: (tag: EjsTag) => ParsedTag = (tag: EjsTag): ParsedTag => {
1893
+ let result: ParsedTag | undefined = parsedTags.get(tag);
1894
+
1895
+ if (!result) {
1896
+ result = parseTag(fileName, tag);
1897
+ parsedTags.set(tag, result);
1898
+
1899
+ if (result.tree) {
1900
+ analysis.findings.push(...result.tree.findings);
1901
+ analysis.variantHiddenSites.push(...result.tree.variantHiddenSites);
1902
+ }
1903
+ }
1904
+
1905
+ return result;
1906
+ };
1907
+
1908
+ // The code in every tag that could hold one of the classes, wherever it is.
1909
+ for (const tag of reading.tags) {
1910
+ if (!tag.isComment && mentionsHidingClass(tag.code)) {
1911
+ parsed(tag);
1912
+ }
1913
+ }
1914
+
1915
+ for (const attribute of reading.attributes) {
1916
+ const staticText: ClassTextUnit = makeUnit(
1917
+ null,
1918
+ attribute.parts,
1919
+ attribute.tags.map((tag: EjsTag): string => {
1920
+ return tag.text;
1921
+ }),
1922
+ );
1923
+
1924
+ if (staticText.tokens.some(isVariantHidden)) {
1925
+ analysis.variantHiddenSites.push({
1926
+ file: fileName,
1927
+ line: attribute.line,
1928
+ text: renderUnit(staticText),
1929
+ });
1930
+ }
1931
+
1932
+ const flaggedStatic: Set<HidingClass> = new Set<HidingClass>();
1933
+
1934
+ for (const rule of HIDING_CLASS_RULES) {
1935
+ const finding: ForeignHiddenRuleFinding | null = sameStringFinding(
1936
+ fileName,
1937
+ attribute.line,
1938
+ staticText,
1939
+ rule,
1940
+ );
1941
+
1942
+ if (finding) {
1943
+ flaggedStatic.add(rule.hidingClass);
1944
+ analysis.findings.push(finding);
1945
+ }
1946
+ }
1947
+
1948
+ const outputTags: Array<EjsTag> = attribute.tags.filter(
1949
+ (tag: EjsTag): boolean => {
1950
+ return tag.outputs;
1951
+ },
1952
+ );
1953
+
1954
+ if (
1955
+ outputTags.length === 0 ||
1956
+ (!mentionsHidingClass(renderUnit(staticText)) &&
1957
+ !outputTags.some((tag: EjsTag): boolean => {
1958
+ return mentionsHidingClass(tag.code);
1959
+ }))
1960
+ ) {
1961
+ continue;
1962
+ }
1963
+
1964
+ /*
1965
+ * The pieces of the value: the static text, which is always there, and
1966
+ * each string an output tag can print, with the branches inside that tag
1967
+ * that decide whether it does. Pieces in different tags always meet.
1968
+ */
1969
+ const placed: Array<PlacedUnit> = [{ unit: staticText, branches: [] }];
1970
+ const treeOf: Map<ClassTextUnit, TreeAnalysis | null> = new Map<
1971
+ ClassTextUnit,
1972
+ TreeAnalysis | null
1973
+ >();
1974
+
1975
+ for (const tag of outputTags) {
1976
+ const parsedTag: ParsedTag = parsed(tag);
1977
+
1978
+ if (!parsedTag.expression) {
1979
+ continue;
1980
+ }
1981
+
1982
+ const pieces: Array<PlacedUnit> = [];
1983
+ collectValueUnits(parsedTag.expression, [], parsedTag.sourceFile, pieces);
1984
+
1985
+ for (const piece of pieces) {
1986
+ treeOf.set(piece.unit, parsedTag.tree);
1987
+ placed.push(piece);
1988
+ }
1989
+ }
1990
+
1991
+ for (const rule of HIDING_CLASS_RULES) {
1992
+ for (const hidden of placed) {
1993
+ if (!carriesBare(hidden.unit, rule)) {
1994
+ continue;
1995
+ }
1996
+
1997
+ // Already reported on its own, or by the reading of its tag's code.
1998
+ const alreadyReported: boolean =
1999
+ hidden.unit === staticText
2000
+ ? flaggedStatic.has(rule.hidingClass)
2001
+ : Boolean(
2002
+ hidden.unit.node &&
2003
+ treeOf
2004
+ .get(hidden.unit)
2005
+ ?.reported.get(rule.hidingClass)
2006
+ ?.has(hidden.unit.node),
2007
+ );
2008
+
2009
+ if (alreadyReported) {
2010
+ continue;
2011
+ }
2012
+
2013
+ const partners: Array<PlacedUnit> = partnersOf(hidden, placed, rule);
2014
+
2015
+ if (partners.length > 0) {
2016
+ analysis.findings.push(
2017
+ crossPieceFinding(fileName, attribute.line, hidden, partners, rule),
2018
+ );
2019
+ }
2020
+ }
2021
+ }
2022
+ }
2023
+
2024
+ sortByLine(analysis.findings);
2025
+ analysis.variantHiddenSites.sort(
2026
+ (first: VariantHiddenSite, second: VariantHiddenSite): number => {
2027
+ return first.line - second.line;
2028
+ },
2029
+ );
2030
+
2031
+ return analysis;
2032
+ }
2033
+
2034
+ /* Reads one file as a view or as a module, by its extension. */
2035
+ export function analyzeFile(fileName: string, source: string): SourceAnalysis {
2036
+ return isMarkupFile(fileName)
2037
+ ? analyzeMarkup(fileName, source)
2038
+ : analyzeSource(fileName, source);
2039
+ }
2040
+
2041
+ /* The findings alone, for callers that do not need the safe sites. */
2042
+ export function findForeignHiddenRuleHazards(
2043
+ fileName: string,
2044
+ source: string,
2045
+ ): Array<ForeignHiddenRuleFinding> {
2046
+ return analyzeFile(fileName, source).findings;
2047
+ }
2048
+
2049
+ /* Reads every file; paths in the result are relative to baseDir. */
2050
+ export function scanFiles(
2051
+ files: Array<string>,
2052
+ baseDir: string,
2053
+ ): ForeignHiddenRuleScan {
2054
+ const scan: ForeignHiddenRuleScan = {
2055
+ files: [],
2056
+ findings: [],
2057
+ variantHiddenSites: [],
2058
+ };
2059
+
2060
+ for (const file of files) {
2061
+ const relative: string = toRelativePath(baseDir, file);
2062
+ const analysis: SourceAnalysis = analyzeFile(
2063
+ relative,
2064
+ fs.readFileSync(file, "utf8"),
2065
+ );
2066
+
2067
+ scan.files.push(relative);
2068
+ scan.findings.push(...analysis.findings);
2069
+ scan.variantHiddenSites.push(...analysis.variantHiddenSites);
2070
+ }
2071
+
2072
+ return scan;
2073
+ }
2074
+
2075
+ function crossPieceNote(finding: ForeignHiddenRuleFinding): string {
2076
+ const where: string = isMarkupFile(finding.file)
2077
+ ? "one class attribute (its static text and a <% %> tag)"
2078
+ : "one class expression";
2079
+
2080
+ return ` (the bare \`${finding.hidingClass}\` and the utility that shows the element again are in different pieces of ${where})`;
2081
+ }
2082
+
2083
+ /*
2084
+ * The failure message: every offending string with its file:line and what
2085
+ * to write instead, then why. Empty when there is nothing to report, so a
2086
+ * suite can assert `toBe("")` and get the whole message as the diff.
2087
+ */
2088
+ export function formatFindings(
2089
+ findings: Array<ForeignHiddenRuleFinding>,
2090
+ ): string {
2091
+ if (findings.length === 0) {
2092
+ return "";
2093
+ }
2094
+
2095
+ const countOf: (hidingClass: HidingClass) => number = (
2096
+ hidingClass: HidingClass,
2097
+ ): number => {
2098
+ return findings.filter((finding: ForeignHiddenRuleFinding): boolean => {
2099
+ return finding.hidingClass === hidingClass;
2100
+ }).length;
2101
+ };
2102
+
2103
+ const hiddenCount: number = countOf("hidden");
2104
+ const srOnlyCount: number = countOf("sr-only");
2105
+ const summaries: Array<string> = [];
2106
+
2107
+ if (hiddenCount > 0) {
2108
+ summaries.push(
2109
+ `${hiddenCount} class string(s) hide an element with the bare \`hidden\` class and rely on a variant utility to show it again. A foreign \`.hidden{display:none !important}\` rule keeps every one of them hidden at every width. Use \`max-<bp>:hidden <bp>:<display>\` instead of \`hidden <bp>:<display>\`${srOnlyCount > 0 ? "." : ":"}`,
2110
+ );
2111
+ }
2112
+
2113
+ if (srOnlyCount > 0) {
2114
+ summaries.push(
2115
+ `${srOnlyCount} class string(s) visually hide an element with the bare \`sr-only\` class and rely on a media-query \`not-sr-only\` to show it again. A foreign \`.sr-only\` rule appended after Tailwind keeps every one of them collapsed to 1px at every width. Use \`max-<bp>:sr-only <bp>:not-sr-only\` instead of \`sr-only <bp>:not-sr-only\`:`,
2116
+ );
2117
+ }
2118
+
2119
+ const entries: Array<string> = findings.map(
2120
+ (finding: ForeignHiddenRuleFinding): string => {
2121
+ const lines: Array<string> = [
2122
+ ` ${finding.file}:${finding.line}${
2123
+ finding.kind === "cross-piece" ? crossPieceNote(finding) : ""
2124
+ }`,
2125
+ ` offending string: "${finding.text}"`,
2126
+ ];
2127
+
2128
+ if (finding.partnerText !== null) {
2129
+ lines.push(` other piece: "${finding.partnerText}"`);
2130
+ }
2131
+
2132
+ lines.push(` shown again by: ${finding.unhidingTokens.join(" ")}`);
2133
+
2134
+ if (finding.replacement !== null) {
2135
+ lines.push(` replace with: "${finding.replacement}"`);
2136
+ }
2137
+
2138
+ lines.push(` fix: ${finding.advice}`);
2139
+
2140
+ return lines.join("\n");
2141
+ },
2142
+ );
2143
+
2144
+ return [
2145
+ summaries.join("\n"),
2146
+ "",
2147
+ entries.join("\n\n"),
2148
+ "",
2149
+ ...(srOnlyCount > 0 ? [SR_ONLY_WHY_PARAGRAPH] : []),
2150
+ ...(hiddenCount > 0 ? [WHY_PARAGRAPH] : []),
2151
+ ].join("\n");
2152
+ }