@flamingo-stack/openframe-frontend-core 0.0.777 → 0.0.781
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/export-map.json +1 -1
- package/dist/case-study-card-CYJ3JXU6.cjs +23 -0
- package/dist/{chunk-64KBI5MI.cjs → chunk-2OYKOHFK.cjs} +2 -2
- package/dist/{chunk-JB2SEF6Q.cjs → chunk-3IHGS44W.cjs} +5 -5
- package/dist/{chunk-E76ONBKF.cjs → chunk-3VINTJPE.cjs} +5 -5
- package/dist/{chunk-R6BKQKIF.cjs → chunk-4XDYSGRL.cjs} +23 -17
- package/dist/{chunk-WMZIBRTR.cjs → chunk-57NE7SHG.cjs} +8 -10
- package/dist/{chunk-G2N7INFT.cjs → chunk-5PBWAF2D.cjs} +44 -44
- package/dist/{chunk-WEANWBJH.cjs → chunk-63OCO2G5.cjs} +4 -3
- package/dist/{chunk-XVAVQ4BK.cjs → chunk-A7FSNBNV.cjs} +217 -275
- package/dist/{chunk-BNUZKBNV.cjs → chunk-AFJEUBDS.cjs} +61 -33
- package/dist/{chunk-XEYL6AFZ.cjs → chunk-AKCBLZAO.cjs} +350 -79
- package/dist/{chunk-DOM5CUEY.cjs → chunk-B6EQVH7D.cjs} +53 -66
- package/dist/{chunk-TF5D5MNO.cjs → chunk-CDBFQVPA.cjs} +84 -127
- package/dist/{chunk-4JZDJ34E.cjs → chunk-D46WFWWI.cjs} +63 -75
- package/dist/{chunk-WCSWWVPF.cjs → chunk-DIY4SJZI.cjs} +20 -30
- package/dist/{chunk-3LW5TD54.cjs → chunk-DJTPDK3W.cjs} +4 -4
- package/dist/{chunk-ALA4KHTE.cjs → chunk-EYNQE2XN.cjs} +6 -6
- package/dist/{chunk-TZP7PSPG.cjs → chunk-FWJ5YECN.cjs} +1312 -1489
- package/dist/{chunk-S3YDUX2I.cjs → chunk-HLZUBJPL.cjs} +600 -646
- package/dist/{chunk-T47VAUYR.cjs → chunk-HPGCUYAZ.cjs} +207 -230
- package/dist/{chunk-SETFUCZA.cjs → chunk-HTSRCFXP.cjs} +7 -7
- package/dist/{chunk-TUAATMUE.cjs → chunk-IYU2ZH2J.cjs} +2 -2
- package/dist/chunk-JHDPEYZH.cjs +385 -0
- package/dist/{chunk-MQPTOJMF.cjs → chunk-JQWYXC2E.cjs} +161 -177
- package/dist/{chunk-YXDHVHB2.cjs → chunk-KWV23THO.cjs} +34 -17
- package/dist/{chunk-XIFEZ2JT.cjs → chunk-L4XOCV7S.cjs} +10 -10
- package/dist/{chunk-U5VQFKRZ.cjs → chunk-LDGR5VCK.cjs} +4 -4
- package/dist/chunk-N6YCOBLK.cjs +252 -0
- package/dist/{chunk-HXLOCZMG.cjs → chunk-ODZ5XGWA.cjs} +4624 -5079
- package/dist/{chunk-7LJSGOWS.cjs → chunk-OFX6XEVN.cjs} +13 -13
- package/dist/{chunk-CGQ7CL62.cjs → chunk-OHXB6HUX.cjs} +808 -422
- package/dist/{chunk-UW6AOF42.cjs → chunk-PID7LUDP.cjs} +59 -57
- package/dist/{chunk-3ZLPYEL5.cjs → chunk-Q7HXTF2C.cjs} +4 -4
- package/dist/{chunk-KKAY6GPU.cjs → chunk-QD7SKYCQ.cjs} +3 -3
- package/dist/{chunk-SDZG7VV7.cjs → chunk-RJX33Y4Q.cjs} +24 -25
- package/dist/{chunk-BEMGJDXI.cjs → chunk-RYIWADIB.cjs} +23 -20
- package/dist/chunk-SMEX3BWO.cjs +49 -0
- package/dist/{chunk-FL2RMEDX.cjs → chunk-T4ZVKEDP.cjs} +10 -7
- package/dist/{chunk-LBGAHB6X.cjs → chunk-T7SDYBST.cjs} +104 -104
- package/dist/{chunk-LU2DGST4.cjs → chunk-TH5X2BXK.cjs} +161 -167
- package/dist/{chunk-VY3NGJRH.cjs → chunk-TZ6MTHUC.cjs} +69 -88
- package/dist/{chunk-SIDDZZKL.cjs → chunk-UONQCE5N.cjs} +348 -373
- package/dist/{chunk-BDDOC425.cjs → chunk-VDTLVFCV.cjs} +13 -13
- package/dist/{chunk-LELS4XMX.cjs → chunk-VZKAAXU3.cjs} +54 -56
- package/dist/{chunk-3PCSEZNZ.cjs → chunk-X6TXMWBV.cjs} +64 -67
- package/dist/{chunk-JMFF3L3T.cjs → chunk-XHJI7R4J.cjs} +55 -85
- package/dist/{chunk-QIATO42V.cjs → chunk-XYKVOBIP.cjs} +3 -3
- package/dist/{chunk-KLTUTZBO.cjs → chunk-YEJWNHD3.cjs} +206 -207
- package/dist/{chunk-T7NDPPLF.cjs → chunk-YKJ4JN6T.cjs} +44 -44
- package/dist/{chunk-XFRBEXQN.cjs → chunk-YNDH72VU.cjs} +21 -48
- package/dist/{chunk-Y6MQBLTZ.cjs → chunk-ZPYFPLPD.cjs} +10 -10
- package/dist/{chunk-5PK22JP7.cjs → chunk-ZV6RQNXE.cjs} +59 -61
- package/dist/{cloud-tenants-EUH42CCL.cjs → cloud-tenants-UAFVHKX7.cjs} +32 -33
- package/dist/components/agent-mark.d.ts +4 -6
- package/dist/components/agent-mark.d.ts.map +1 -1
- package/dist/components/agent-mark.js +3 -2
- package/dist/components/authors/author-detail-view.js +3 -5
- package/dist/components/calendar.cjs +11 -11
- package/dist/components/case-studies/entity-quotes-panel.d.ts +0 -7
- package/dist/components/case-studies/entity-quotes-panel.d.ts.map +1 -1
- package/dist/components/case-studies/entity-quotes-panel.js +1 -25
- package/dist/components/case-studies/index.cjs +57 -84
- package/dist/components/case-studies/index.d.ts +2 -2
- package/dist/components/case-studies/index.d.ts.map +1 -1
- package/dist/components/case-studies/index.js +1 -4
- package/dist/components/case-studies/quote-cards-strip.d.ts +13 -9
- package/dist/components/case-studies/quote-cards-strip.d.ts.map +1 -1
- package/dist/components/case-studies/quote-cards-strip.js +7 -7
- package/dist/components/categories-cart.d.ts +0 -9
- package/dist/components/categories-cart.d.ts.map +1 -1
- package/dist/components/categories-cart.js +24 -9
- package/dist/components/category-card.d.ts.map +1 -1
- package/dist/components/category-card.js +57 -11
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/approval-batch-message.js +14 -22
- package/dist/components/chat/ask-display.d.ts.map +1 -1
- package/dist/components/chat/ask-display.js +9 -10
- package/dist/components/chat/ask-prompts.d.ts +3 -3
- package/dist/components/chat/ask-prompts.d.ts.map +1 -1
- package/dist/components/chat/ask-prompts.js +20 -20
- package/dist/components/chat/chat-archive-page.d.ts +5 -4
- package/dist/components/chat/chat-archive-page.d.ts.map +1 -1
- package/dist/components/chat/chat-archive-page.js +33 -22
- package/dist/components/chat/chat-container.d.ts.map +1 -1
- package/dist/components/chat/chat-container.js +18 -15
- package/dist/components/chat/chat-context-memory-bar.d.ts.map +1 -1
- package/dist/components/chat/chat-context-memory-bar.js +10 -9
- package/dist/components/chat/chat-context-picker.d.ts.map +1 -1
- package/dist/components/chat/chat-context-picker.js +40 -35
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-input.js +5 -13
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.js +33 -16
- package/dist/components/chat/chat-message-layout.d.ts +35 -0
- package/dist/components/chat/chat-message-layout.d.ts.map +1 -0
- package/dist/components/chat/chat-message-layout.js +34 -0
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.js +30 -16
- package/dist/components/chat/chat-message-row.d.ts +9 -8
- package/dist/components/chat/chat-message-row.d.ts.map +1 -1
- package/dist/components/chat/chat-message-row.js +40 -19
- package/dist/components/chat/chat-message-skeleton.d.ts +34 -13
- package/dist/components/chat/chat-message-skeleton.d.ts.map +1 -1
- package/dist/components/chat/chat-message-skeleton.js +57 -29
- package/dist/components/chat/chat-nav-sidebar.d.ts +20 -2
- package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -1
- package/dist/components/chat/chat-nav-sidebar.js +66 -34
- package/dist/components/chat/chat-panel-header.d.ts.map +1 -1
- package/dist/components/chat/chat-panel-header.js +18 -8
- package/dist/components/chat/chat-sidebar.d.ts +4 -0
- package/dist/components/chat/chat-sidebar.d.ts.map +1 -1
- package/dist/components/chat/chat-sidebar.js +34 -26
- package/dist/components/chat/chat-ticket-list.d.ts +4 -1
- package/dist/components/chat/chat-ticket-list.d.ts.map +1 -1
- package/dist/components/chat/chat-ticket-list.js +18 -13
- package/dist/components/chat/chat-top-navigation.d.ts +2 -1
- package/dist/components/chat/chat-top-navigation.d.ts.map +1 -1
- package/dist/components/chat/chat-top-navigation.js +55 -47
- package/dist/components/chat/chat-turn.d.ts +11 -11
- package/dist/components/chat/chat-turn.d.ts.map +1 -1
- package/dist/components/chat/chat-turn.js +9 -13
- package/dist/components/chat/context-items-list.d.ts +11 -7
- package/dist/components/chat/context-items-list.d.ts.map +1 -1
- package/dist/components/chat/context-items-list.js +53 -51
- package/dist/components/chat/embeddable-chat.d.ts +8 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.js +81 -81
- package/dist/components/chat/entity-cards/admin-content-card-grid.d.ts +8 -29
- package/dist/components/chat/entity-cards/admin-content-card-grid.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/admin-content-card-grid.js +27 -22
- package/dist/components/chat/entity-cards/admin-content-card.d.ts +6 -15
- package/dist/components/chat/entity-cards/admin-content-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/admin-content-card.js +9 -8
- package/dist/components/chat/entity-cards/ai-prompt-card.d.ts +10 -5
- package/dist/components/chat/entity-cards/ai-prompt-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/ai-prompt-card.js +5 -9
- package/dist/components/chat/entity-cards/blog-card.d.ts +11 -11
- package/dist/components/chat/entity-cards/blog-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/blog-card.js +20 -6
- package/dist/components/chat/entity-cards/campaign-card-admin.d.ts +0 -3
- package/dist/components/chat/entity-cards/campaign-card-admin.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/campaign-card-admin.js +1 -2
- package/dist/components/chat/entity-cards/case-study-card.d.ts +0 -10
- package/dist/components/chat/entity-cards/case-study-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/case-study-card.js +13 -4
- package/dist/components/chat/entity-cards/chat-ticket-item.d.ts +6 -8
- package/dist/components/chat/entity-cards/chat-ticket-item.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/chat-ticket-item.js +69 -85
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts +11 -11
- package/dist/components/chat/entity-cards/customer-interview-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/customer-interview-card.js +48 -21
- package/dist/components/chat/entity-cards/data-room-doc-card.d.ts +0 -3
- package/dist/components/chat/entity-cards/data-room-doc-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/data-room-doc-card.js +1 -2
- package/dist/components/chat/entity-cards/design-doc-card.d.ts +0 -8
- package/dist/components/chat/entity-cards/design-doc-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/design-doc-card.js +5 -6
- package/dist/components/chat/entity-cards/dispatch.d.ts +2 -2
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.js +23 -26
- package/dist/components/chat/entity-cards/employee-entry-card.d.ts +14 -21
- package/dist/components/chat/entity-cards/employee-entry-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/employee-entry-card.js +19 -7
- package/dist/components/chat/entity-cards/entity-author-card.d.ts +13 -29
- package/dist/components/chat/entity-cards/entity-author-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/entity-author-card.js +7 -8
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts +4 -10
- package/dist/components/chat/entity-cards/entity-portrait-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/entity-portrait-card.js +22 -13
- package/dist/components/chat/entity-cards/generic-entity-card.d.ts +0 -3
- package/dist/components/chat/entity-cards/generic-entity-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/generic-entity-card.js +1 -2
- package/dist/components/chat/entity-cards/github-activity-card.d.ts +0 -4
- package/dist/components/chat/entity-cards/github-activity-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/github-activity-card.js +0 -1
- package/dist/components/chat/entity-cards/how-i-work-card.d.ts +10 -6
- package/dist/components/chat/entity-cards/how-i-work-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/how-i-work-card.js +5 -9
- package/dist/components/chat/entity-cards/hubspot-ticket-card.d.ts +0 -4
- package/dist/components/chat/entity-cards/hubspot-ticket-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/hubspot-ticket-card.js +1 -2
- package/dist/components/chat/entity-cards/index.d.ts +23 -24
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/index.js +26 -78
- package/dist/components/chat/entity-cards/investor-update-card.d.ts +11 -10
- package/dist/components/chat/entity-cards/investor-update-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/investor-update-card.js +16 -10
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts +14 -14
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.js +34 -12
- package/dist/components/chat/entity-cards/program-card.d.ts +18 -17
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/program-card.js +47 -36
- package/dist/components/chat/entity-cards/roadmap-card.d.ts +12 -14
- package/dist/components/chat/entity-cards/roadmap-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/roadmap-card.js +19 -9
- package/dist/components/chat/entity-cards/slack-message-card.d.ts +0 -4
- package/dist/components/chat/entity-cards/slack-message-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/slack-message-card.js +1 -2
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts +10 -6
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/what-i-shipped-card.js +5 -5
- package/dist/components/chat/error-message-display.d.ts.map +1 -1
- package/dist/components/chat/error-message-display.js +3 -5
- package/dist/components/chat/guide-mode-banner.d.ts.map +1 -1
- package/dist/components/chat/guide-mode-banner.js +2 -9
- package/dist/components/chat/guide-welcome.d.ts +1 -1
- package/dist/components/chat/guide-welcome.d.ts.map +1 -1
- package/dist/components/chat/guide-welcome.js +4 -2
- package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-dialog-manager.js +2 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.js +2 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.js +2 -1
- package/dist/components/chat/index.cjs +26 -88
- package/dist/components/chat/index.d.ts +1 -4
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +2 -4
- package/dist/components/chat/mingo-archive-list.d.ts +4 -2
- package/dist/components/chat/mingo-archive-list.d.ts.map +1 -1
- package/dist/components/chat/mingo-archive-list.js +8 -4
- package/dist/components/chat/mingo-chat-header.d.ts.map +1 -1
- package/dist/components/chat/mingo-chat-header.js +9 -8
- package/dist/components/chat/mingo-chat-history.d.ts +15 -14
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-chat-history.js +63 -46
- package/dist/components/chat/mingo-chat-modals.d.ts.map +1 -1
- package/dist/components/chat/mingo-chat-modals.js +10 -12
- package/dist/components/chat/mingo-chat-rail.d.ts +4 -2
- package/dist/components/chat/mingo-chat-rail.d.ts.map +1 -1
- package/dist/components/chat/mingo-chat-rail.js +10 -6
- package/dist/components/chat/mingo-history-rail.d.ts +6 -3
- package/dist/components/chat/mingo-history-rail.d.ts.map +1 -1
- package/dist/components/chat/mingo-history-rail.js +7 -2
- package/dist/components/chat/mingo-info-card.d.ts +6 -7
- package/dist/components/chat/mingo-info-card.d.ts.map +1 -1
- package/dist/components/chat/mingo-info-card.js +21 -16
- package/dist/components/chat/mingo-onboarding-card.d.ts +8 -1
- package/dist/components/chat/mingo-onboarding-card.d.ts.map +1 -1
- package/dist/components/chat/mingo-onboarding-card.js +70 -32
- package/dist/components/chat/mingo-welcome.d.ts +4 -1
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/chat/mingo-welcome.js +15 -5
- package/dist/components/chat/model-display.d.ts +1 -15
- package/dist/components/chat/model-display.d.ts.map +1 -1
- package/dist/components/chat/model-display.js +16 -13
- package/dist/components/chat/msp-organization-card.d.ts +8 -1
- package/dist/components/chat/msp-organization-card.d.ts.map +1 -1
- package/dist/components/chat/msp-organization-card.js +34 -39
- package/dist/components/chat/quick-action-chip.d.ts +10 -24
- package/dist/components/chat/quick-action-chip.d.ts.map +1 -1
- package/dist/components/chat/quick-action-chip.js +42 -8
- package/dist/components/chat/quick-action-wall.d.ts +3 -3
- package/dist/components/chat/quick-action-wall.d.ts.map +1 -1
- package/dist/components/chat/quick-action-wall.js +17 -10
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.js +2 -9
- package/dist/components/chat/types/component.types.d.ts +26 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/entities/program-types.d.ts +3 -0
- package/dist/components/chat/types/entities/program-types.d.ts.map +1 -1
- package/dist/components/chat/utils/compact-card-skeleton.d.ts +1 -1
- package/dist/components/chat/utils/compact-card-skeleton.js +2 -2
- package/dist/components/command.d.ts +7 -7
- package/dist/components/comment-card.d.ts +15 -16
- package/dist/components/comment-card.d.ts.map +1 -1
- package/dist/components/comment-card.js +121 -127
- package/dist/components/comment-thread.d.ts +9 -27
- package/dist/components/comment-thread.d.ts.map +1 -1
- package/dist/components/comment-thread.js +36 -24
- package/dist/components/contact/index.cjs +33 -34
- package/dist/components/docs/docs-hub-page.d.ts +1 -4
- package/dist/components/docs/docs-hub-page.d.ts.map +1 -1
- package/dist/components/docs/docs-hub-page.js +2 -3
- package/dist/components/docs/index.cjs +36 -41
- package/dist/components/docs/index.d.ts +0 -1
- package/dist/components/docs/index.d.ts.map +1 -1
- package/dist/components/docs/index.js +0 -3
- package/dist/components/embeds/embed-viewer-frame.d.ts +1 -1
- package/dist/components/embeds/embed-viewer-frame.d.ts.map +1 -1
- package/dist/components/embeds/embed-viewer-frame.js +3 -1
- package/dist/components/embeds/index.cjs +33 -38
- package/dist/components/embeds/index.d.ts +1 -1
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +1 -3
- package/dist/components/embeds/og-link-preview.d.ts +0 -10
- package/dist/components/embeds/og-link-preview.d.ts.map +1 -1
- package/dist/components/embeds/og-link-preview.js +1 -3
- package/dist/components/faq/faq-section.d.ts +0 -7
- package/dist/components/faq/faq-section.d.ts.map +1 -1
- package/dist/components/faq/faq-section.js +13 -9
- package/dist/components/faq/index.cjs +34 -37
- package/dist/components/faq/index.d.ts +1 -1
- package/dist/components/faq/index.d.ts.map +1 -1
- package/dist/components/faq/index.js +0 -2
- package/dist/components/faq-accordion.d.ts +6 -9
- package/dist/components/faq-accordion.d.ts.map +1 -1
- package/dist/components/faq-accordion.js +14 -17
- package/dist/components/features/ai-enrich/AIEnrichSection.d.ts.map +1 -1
- package/dist/components/features/ai-enrich/AIEnrichSection.js +2 -1
- package/dist/components/features/approval-level.d.ts +0 -12
- package/dist/components/features/approval-level.d.ts.map +1 -1
- package/dist/components/features/approval-level.js +0 -1
- package/dist/components/features/auth/back-to-login-link.js +2 -2
- package/dist/components/features/auth/complete-account-form.d.ts +4 -1
- package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
- package/dist/components/features/auth/complete-account-form.js +5 -3
- package/dist/components/features/auth/create-organization-form.d.ts +5 -3
- package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
- package/dist/components/features/auth/create-organization-form.js +12 -2
- package/dist/components/features/auth/invite-link-invalid-modal.d.ts.map +1 -1
- package/dist/components/features/auth/invite-link-invalid-modal.js +5 -3
- package/dist/components/features/auth/login-form.d.ts +6 -1
- package/dist/components/features/auth/login-form.d.ts.map +1 -1
- package/dist/components/features/auth/login-form.js +5 -3
- package/dist/components/features/auth/sso-join-form.d.ts +7 -10
- package/dist/components/features/auth/sso-join-form.d.ts.map +1 -1
- package/dist/components/features/auth/sso-join-form.js +16 -6
- package/dist/components/features/auth/sso-providers.d.ts +9 -1
- package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
- package/dist/components/features/auth/sso-providers.js +14 -2
- package/dist/components/features/board/board-column.d.ts +1 -1
- package/dist/components/features/board/board-column.d.ts.map +1 -1
- package/dist/components/features/board/board-column.js +9 -7
- package/dist/components/features/board/board-ticket-approval.d.ts.map +1 -1
- package/dist/components/features/board/board-ticket-approval.js +16 -24
- package/dist/components/features/board/board.d.ts +5 -3
- package/dist/components/features/board/board.d.ts.map +1 -1
- package/dist/components/features/board/board.js +17 -8
- package/dist/components/features/board/index.d.ts +1 -3
- package/dist/components/features/board/index.d.ts.map +1 -1
- package/dist/components/features/board/index.js +2 -3
- package/dist/components/features/board/ticket-card.d.ts +9 -6
- package/dist/components/features/board/ticket-card.d.ts.map +1 -1
- package/dist/components/features/board/ticket-card.js +41 -8
- package/dist/components/features/board/types.d.ts +5 -0
- package/dist/components/features/board/types.d.ts.map +1 -1
- package/dist/components/features/board/types.js +2 -0
- package/dist/components/features/capability-explorer.d.ts +2 -16
- package/dist/components/features/capability-explorer.d.ts.map +1 -1
- package/dist/components/features/capability-explorer.js +106 -121
- package/dist/components/features/card-hit-layer.d.ts.map +1 -1
- package/dist/components/features/card-hit-layer.js +4 -2
- package/dist/components/features/cloud-tenants/tenant-access-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-access-cell.js +4 -2
- package/dist/components/features/cloud-tenants/tenant-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-cell.js +5 -2
- package/dist/components/features/cloud-tenants/tenant-customer-cell.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenant-customer-cell.js +3 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.d.ts.map +1 -1
- package/dist/components/features/cloud-tenants/tenants-table-view.js +39 -20
- package/dist/components/features/command-box.d.ts +4 -20
- package/dist/components/features/command-box.d.ts.map +1 -1
- package/dist/components/features/command-box.js +28 -8
- package/dist/components/features/conversation-card.d.ts +3 -28
- package/dist/components/features/conversation-card.d.ts.map +1 -1
- package/dist/components/features/conversation-card.js +73 -30
- package/dist/components/features/devices/device-selector/device-selector.d.ts +1 -1
- package/dist/components/features/devices/device-selector/device-selector.d.ts.map +1 -1
- package/dist/components/features/devices/device-selector/device-selector.js +45 -45
- package/dist/components/features/devices/device-selector/device-selector.types.d.ts +5 -0
- package/dist/components/features/devices/device-selector/device-selector.types.d.ts.map +1 -1
- package/dist/components/features/devices/device-type-tile.d.ts.map +1 -1
- package/dist/components/features/devices/device-type-tile.js +4 -1
- package/dist/components/features/devices/devices-grid-filters.d.ts.map +1 -1
- package/dist/components/features/devices/devices-grid-filters.js +5 -20
- package/dist/components/features/devices/devices-grid.d.ts +8 -1
- package/dist/components/features/devices/devices-grid.d.ts.map +1 -1
- package/dist/components/features/devices/devices-grid.js +22 -12
- package/dist/components/features/devices/devices-list.d.ts +1 -1
- package/dist/components/features/devices/devices-list.d.ts.map +1 -1
- package/dist/components/features/devices/devices-list.js +5 -2
- package/dist/components/features/devices/devices-panel-view.d.ts +7 -1
- package/dist/components/features/devices/devices-panel-view.d.ts.map +1 -1
- package/dist/components/features/devices/devices-panel-view.js +11 -6
- package/dist/components/features/devices/devices-table-columns.d.ts.map +1 -1
- package/dist/components/features/devices/devices-table-columns.js +29 -8
- package/dist/components/features/devices/index.d.ts +0 -1
- package/dist/components/features/devices/index.d.ts.map +1 -1
- package/dist/components/features/devices/index.js +0 -2
- package/dist/components/features/entity-tag-badges.d.ts +3 -20
- package/dist/components/features/entity-tag-badges.d.ts.map +1 -1
- package/dist/components/features/entity-tag-badges.js +12 -27
- package/dist/components/features/entity-video-section.d.ts +0 -8
- package/dist/components/features/entity-video-section.d.ts.map +1 -1
- package/dist/components/features/entity-video-section.js +4 -5
- package/dist/components/features/filters-dropdown.d.ts.map +1 -1
- package/dist/components/features/filters-dropdown.js +14 -17
- package/dist/components/features/highlight-video-section.d.ts.map +1 -1
- package/dist/components/features/highlight-video-section.js +7 -7
- package/dist/components/features/index.cjs +32 -65
- package/dist/components/features/knowledge-base/knowledge-base-table.d.ts.map +1 -1
- package/dist/components/features/knowledge-base/knowledge-base-table.js +78 -47
- package/dist/components/features/logs/index.d.ts +2 -2
- package/dist/components/features/logs/index.d.ts.map +1 -1
- package/dist/components/features/logs/index.js +1 -2
- package/dist/components/features/logs/logs-page-view.d.ts.map +1 -1
- package/dist/components/features/logs/logs-page-view.js +1 -0
- package/dist/components/features/logs/logs-table-columns.d.ts +2 -3
- package/dist/components/features/logs/logs-table-columns.d.ts.map +1 -1
- package/dist/components/features/logs/logs-table-view.d.ts +6 -12
- package/dist/components/features/logs/logs-table-view.d.ts.map +1 -1
- package/dist/components/features/logs/logs-table-view.js +75 -98
- package/dist/components/features/model-token-rates.d.ts +0 -5
- package/dist/components/features/model-token-rates.d.ts.map +1 -1
- package/dist/components/features/model-token-rates.js +18 -20
- package/dist/components/features/notifications/index.d.ts +2 -2
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/index.js +1 -2
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-drawer.js +6 -3
- package/dist/components/features/notifications/notification-tile.d.ts +21 -20
- package/dist/components/features/notifications/notification-tile.d.ts.map +1 -1
- package/dist/components/features/notifications/notification-tile.js +24 -6
- package/dist/components/features/os-type-badge-group.d.ts +3 -1
- package/dist/components/features/os-type-badge-group.d.ts.map +1 -1
- package/dist/components/features/os-type-badge-group.js +12 -1
- package/dist/components/features/os-type-badge.d.ts.map +1 -1
- package/dist/components/features/os-type-badge.js +10 -0
- package/dist/components/features/paths-display.d.ts +0 -7
- package/dist/components/features/paths-display.d.ts.map +1 -1
- package/dist/components/features/paths-display.js +17 -9
- package/dist/components/features/platform-badge.d.ts +4 -3
- package/dist/components/features/platform-badge.d.ts.map +1 -1
- package/dist/components/features/platform-badge.js +4 -4
- package/dist/components/features/policies/policies-table.d.ts.map +1 -1
- package/dist/components/features/policies/policies-table.js +49 -27
- package/dist/components/features/policy-configuration-panel.d.ts +2 -0
- package/dist/components/features/policy-configuration-panel.d.ts.map +1 -1
- package/dist/components/features/policy-configuration-panel.js +56 -34
- package/dist/components/features/push-button-selector.d.ts +0 -19
- package/dist/components/features/push-button-selector.d.ts.map +1 -1
- package/dist/components/features/push-button-selector.js +13 -11
- package/dist/components/features/remote-session/index.d.ts +1 -1
- package/dist/components/features/remote-session/index.d.ts.map +1 -1
- package/dist/components/features/remote-session/index.js +1 -2
- package/dist/components/features/remote-session/remote-desktop-chat-message-row.d.ts +9 -3
- package/dist/components/features/remote-session/remote-desktop-chat-message-row.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-desktop-chat-message-row.js +31 -17
- package/dist/components/features/remote-session/remote-desktop-chat-panel.d.ts +1 -1
- package/dist/components/features/remote-session/remote-desktop-chat-panel.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-desktop-chat-panel.js +20 -7
- package/dist/components/features/remote-session/remote-desktop-fullscreen-toolbar.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-desktop-fullscreen-toolbar.js +5 -13
- package/dist/components/features/remote-session/remote-desktop-view.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-desktop-view.js +7 -15
- package/dist/components/features/remote-session/remote-session-block.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-session-block.js +18 -28
- package/dist/components/features/remote-session/remote-session-chat-panel.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-session-chat-panel.js +5 -3
- package/dist/components/features/remote-session/remote-session-events.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-session-events.js +9 -8
- package/dist/components/features/remote-session/remote-session-overlay.d.ts.map +1 -1
- package/dist/components/features/remote-session/remote-session-overlay.js +5 -12
- package/dist/components/features/scripts/execution-cells.d.ts +4 -1
- package/dist/components/features/scripts/execution-cells.d.ts.map +1 -1
- package/dist/components/features/scripts/execution-cells.js +9 -3
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +4 -2
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -1
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +5 -2
- package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -1
- package/dist/components/features/scripts/script-executions-table.js +24 -7
- package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -1
- package/dist/components/features/scripts/script-schedules-table.js +36 -9
- package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -1
- package/dist/components/features/scripts/script-shell-badge.js +8 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts +4 -1
- package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -1
- package/dist/components/features/scripts/scripts-table-shared.js +4 -1
- package/dist/components/features/scripts/scripts-table.d.ts.map +1 -1
- package/dist/components/features/scripts/scripts-table.js +23 -7
- package/dist/components/features/select-button.d.ts.map +1 -1
- package/dist/components/features/select-button.js +5 -12
- package/dist/components/features/status-filter-component.d.ts +0 -14
- package/dist/components/features/status-filter-component.d.ts.map +1 -1
- package/dist/components/features/status-filter-component.js +1 -2
- package/dist/components/features/tags-selector.d.ts.map +1 -1
- package/dist/components/features/tags-selector.js +4 -3
- package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts +10 -1
- package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts.map +1 -1
- package/dist/components/features/ticket-status-config-list/ticket-status-config-list.js +34 -9
- package/dist/components/features/video-bites-strip.d.ts +0 -31
- package/dist/components/features/video-bites-strip.d.ts.map +1 -1
- package/dist/components/features/video-bites-strip.js +2 -9
- package/dist/components/features/video-hover-preview.d.ts.map +1 -1
- package/dist/components/features/video-hover-preview.js +2 -5
- package/dist/components/features/video-shared.d.ts +3 -0
- package/dist/components/features/video-shared.d.ts.map +1 -1
- package/dist/components/features/video-source-selector.d.ts.map +1 -1
- package/dist/components/features/video-source-selector.js +5 -3
- package/dist/components/features/video.d.ts +1 -9
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/features/video.js +5 -47
- package/dist/components/features/walkthrough-video.d.ts +0 -7
- package/dist/components/features/walkthrough-video.d.ts.map +1 -1
- package/dist/components/features/walkthrough-video.js +1 -2
- package/dist/components/filter-chip.d.ts.map +1 -1
- package/dist/components/filter-chip.js +5 -9
- package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/download-apps-page.js +19 -13
- package/dist/components/help-center-pages/index.cjs +187 -191
- package/dist/components/help-center-pages/index.d.ts +1 -1
- package/dist/components/help-center-pages/index.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.js +2 -3
- package/dist/components/help-center-pages/trust-center-page.d.ts +0 -9
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/trust-center-page.js +4 -11
- package/dist/components/help-center-pages/trust-center-sections.js +10 -10
- package/dist/components/index.cjs +493 -1298
- package/dist/components/index.d.ts +2 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -9
- package/dist/components/interactive-wrapper.d.ts +0 -4
- package/dist/components/interactive-wrapper.d.ts.map +1 -1
- package/dist/components/interactive-wrapper.js +0 -21
- package/dist/components/layout/back-button.d.ts +5 -2
- package/dist/components/layout/back-button.d.ts.map +1 -1
- package/dist/components/layout/back-button.js +5 -18
- package/dist/components/layout/list-page-layout.d.ts +3 -3
- package/dist/components/layout/list-page-layout.d.ts.map +1 -1
- package/dist/components/layout/list-page-layout.js +1 -0
- package/dist/components/layout/page-container.d.ts +0 -8
- package/dist/components/layout/page-container.d.ts.map +1 -1
- package/dist/components/layout/page-container.js +0 -2
- package/dist/components/layout/page-heading.d.ts +11 -21
- package/dist/components/layout/page-heading.d.ts.map +1 -1
- package/dist/components/layout/page-heading.js +18 -2
- package/dist/components/loading/index.d.ts +2 -13
- package/dist/components/loading/index.d.ts.map +1 -1
- package/dist/components/loading/index.js +5 -42
- package/dist/components/loading/loading-fallback.d.ts +46 -0
- package/dist/components/loading/loading-fallback.d.ts.map +1 -0
- package/dist/components/loading/loading-fallback.js +33 -0
- package/dist/components/loading/loading-region.d.ts.map +1 -1
- package/dist/components/loading/loading-region.js +2 -2
- package/dist/components/log-severity-dot.d.ts +0 -2
- package/dist/components/log-severity-dot.d.ts.map +1 -1
- package/dist/components/log-severity-dot.js +4 -3
- package/dist/components/logs-list.d.ts.map +1 -1
- package/dist/components/logs-list.js +6 -8
- package/dist/components/meeting-scheduler/booking-form.d.ts +1 -28
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/booking-form.js +4 -6
- package/dist/components/meeting-scheduler/context-panel.d.ts +1 -24
- package/dist/components/meeting-scheduler/context-panel.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/context-panel.js +8 -39
- package/dist/components/meeting-scheduler/directory.d.ts +0 -10
- package/dist/components/meeting-scheduler/directory.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/directory.js +6 -7
- package/dist/components/meeting-scheduler/host-identity.d.ts +12 -0
- package/dist/components/meeting-scheduler/host-identity.d.ts.map +1 -0
- package/dist/components/meeting-scheduler/host-identity.js +51 -0
- package/dist/components/meeting-scheduler/index.cjs +278 -306
- package/dist/components/meeting-scheduler/index.d.ts +3 -3
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +4 -11
- package/dist/components/meeting-scheduler/modal-header.d.ts +1 -4
- package/dist/components/meeting-scheduler/modal-header.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/modal-header.js +6 -45
- package/dist/components/meeting-scheduler/slot-picker.d.ts +0 -16
- package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/slot-picker.js +7 -14
- package/dist/components/metric-value.d.ts +0 -5
- package/dist/components/metric-value.d.ts.map +1 -1
- package/dist/components/metric-value.js +1 -2
- package/dist/components/msp-display.d.ts +0 -5
- package/dist/components/msp-display.d.ts.map +1 -1
- package/dist/components/msp-display.js +3 -4
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/app-header.js +3 -1
- package/dist/components/navigation/header-button.d.ts.map +1 -1
- package/dist/components/navigation/header-button.js +5 -9
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.js +4 -8
- package/dist/components/navigation/header-organization-filter.d.ts.map +1 -1
- package/dist/components/navigation/header-organization-filter.js +11 -23
- package/dist/components/navigation/index.cjs +35 -36
- package/dist/components/navigation/mobile-burger-menu.d.ts.map +1 -1
- package/dist/components/navigation/mobile-burger-menu.js +8 -29
- package/dist/components/navigation/multi-level-navigation.d.ts +0 -10
- package/dist/components/navigation/multi-level-navigation.d.ts.map +1 -1
- package/dist/components/navigation/multi-level-navigation.js +26 -37
- package/dist/components/navigation/navigation-sidebar-item.d.ts +12 -14
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar-item.js +29 -44
- package/dist/components/navigation/navigation-sidebar-toggle.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar-toggle.js +4 -8
- package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar.js +3 -3
- package/dist/components/navigation/sticky-section-nav.d.ts +0 -7
- package/dist/components/navigation/sticky-section-nav.d.ts.map +1 -1
- package/dist/components/navigation/sticky-section-nav.js +9 -15
- package/dist/components/onboarding-guides/index.cjs +35 -36
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.js +3 -5
- package/dist/components/onboarding-guides/onboarding-guides-catalog-view.d.ts.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guides-catalog-view.js +22 -14
- package/dist/components/pagination.d.ts.map +1 -1
- package/dist/components/pagination.js +1 -11
- package/dist/components/platform/ToolBadge.d.ts +0 -8
- package/dist/components/platform/ToolBadge.d.ts.map +1 -1
- package/dist/components/platform/ToolBadge.js +1 -2
- package/dist/components/platform/index.d.ts +2 -2
- package/dist/components/platform/index.d.ts.map +1 -1
- package/dist/components/platform/index.js +2 -3
- package/dist/components/product-demo/index.cjs +66 -54
- package/dist/components/product-demo/product-screen.d.ts.map +1 -1
- package/dist/components/product-demo/product-screen.js +22 -9
- package/dist/components/product-demo/screen-loading.d.ts.map +1 -1
- package/dist/components/product-demo/screen-loading.js +2 -4
- package/dist/components/related-content/card-registry.d.ts.map +1 -1
- package/dist/components/related-content/card-registry.js +23 -36
- package/dist/components/related-content/index.cjs +17 -19
- package/dist/components/related-content/index.d.ts +1 -1
- package/dist/components/related-content/index.d.ts.map +1 -1
- package/dist/components/related-content/index.js +2 -4
- package/dist/components/related-content/related-content-section.d.ts +0 -14
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/related-content/related-content-section.js +1 -2
- package/dist/components/resizable.d.ts +1 -1
- package/dist/components/results-count.d.ts +0 -5
- package/dist/components/results-count.d.ts.map +1 -1
- package/dist/components/results-count.js +1 -2
- package/dist/components/shared/article-author-byline.d.ts +0 -8
- package/dist/components/shared/article-author-byline.d.ts.map +1 -1
- package/dist/components/shared/article-author-byline.js +1 -2
- package/dist/components/shared/delivery/delivery-row.d.ts +12 -8
- package/dist/components/shared/delivery/delivery-row.d.ts.map +1 -1
- package/dist/components/shared/delivery/delivery-row.js +7 -7
- package/dist/components/shared/delivery/delivery-table.d.ts +1 -3
- package/dist/components/shared/delivery/delivery-table.d.ts.map +1 -1
- package/dist/components/shared/delivery/delivery-table.js +6 -6
- package/dist/components/shared/dev-section/dev-card-row.d.ts +10 -22
- package/dist/components/shared/dev-section/dev-card-row.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-card-row.js +10 -39
- package/dist/components/shared/dev-section/index.d.ts +1 -1
- package/dist/components/shared/dev-section/index.d.ts.map +1 -1
- package/dist/components/shared/dev-section/index.js +1 -7
- package/dist/components/shared/legal-document/index.d.ts +1 -1
- package/dist/components/shared/legal-document/index.d.ts.map +1 -1
- package/dist/components/shared/legal-document/index.js +0 -2
- package/dist/components/shared/legal-document/legal-document-page.d.ts +0 -7
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.js +2 -3
- package/dist/components/shared/media-gallery-strip.d.ts +0 -5
- package/dist/components/shared/media-gallery-strip.d.ts.map +1 -1
- package/dist/components/shared/media-gallery-strip.js +15 -14
- package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts +1 -1
- package/dist/components/shared/onboarding/onboarding-walkthrough.d.ts.map +1 -1
- package/dist/components/shared/onboarding/onboarding-walkthrough.js +3 -1
- package/dist/components/shared/product-release/index.d.ts +0 -1
- package/dist/components/shared/product-release/index.d.ts.map +1 -1
- package/dist/components/shared/product-release/index.js +0 -2
- package/dist/components/shared/product-release/product-release-card-skeleton.js +4 -4
- package/dist/components/shared/product-release/product-release-card.d.ts +9 -4
- package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
- package/dist/components/shared/product-release/product-release-card.js +8 -4
- package/dist/components/shared/product-release/product-releases-view.d.ts +3 -3
- package/dist/components/shared/product-release/product-releases-view.d.ts.map +1 -1
- package/dist/components/shared/product-release/product-releases-view.js +8 -8
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.js +3 -5
- package/dist/components/shared/roadmap/index.d.ts +0 -1
- package/dist/components/shared/roadmap/index.d.ts.map +1 -1
- package/dist/components/shared/roadmap/index.js +0 -2
- package/dist/components/shared/roadmap/roadmap-grid.d.ts +2 -12
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-grid.js +16 -7
- package/dist/components/shared/roadmap/roadmap-view.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-view.js +3 -2
- package/dist/components/smooth-accordion.d.ts.map +1 -1
- package/dist/components/smooth-accordion.js +0 -2
- package/dist/components/social-icon-row.d.ts +6 -1
- package/dist/components/social-icon-row.d.ts.map +1 -1
- package/dist/components/social-icon-row.js +5 -1
- package/dist/components/tickets/help-center-card.d.ts +16 -2
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/help-center-card.js +28 -9
- package/dist/components/tickets/help-center-create-form.d.ts +15 -37
- package/dist/components/tickets/help-center-create-form.d.ts.map +1 -1
- package/dist/components/tickets/help-center-create-form.js +10 -41
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.js +27 -40
- package/dist/components/tickets/index.cjs +39 -41
- package/dist/components/tickets/index.d.ts +1 -1
- package/dist/components/tickets/index.d.ts.map +1 -1
- package/dist/components/tickets/index.js +1 -5
- package/dist/components/tickets/ticket-center.d.ts.map +1 -1
- package/dist/components/tickets/ticket-center.js +6 -14
- package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
- package/dist/components/tickets/ticket-detail-drawer.js +5 -2
- package/dist/components/tickets/ticket-open-form.d.ts.map +1 -1
- package/dist/components/tickets/ticket-open-form.js +3 -2
- package/dist/components/tickets/ticket-row.d.ts +17 -2
- package/dist/components/tickets/ticket-row.d.ts.map +1 -1
- package/dist/components/tickets/ticket-row.js +13 -3
- package/dist/components/ui/actions-menu.d.ts +3 -0
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/actions-menu.js +36 -39
- package/dist/components/ui/alert.d.ts +1 -1
- package/dist/components/ui/allowed-domains-input.d.ts.map +1 -1
- package/dist/components/ui/allowed-domains-input.js +7 -10
- package/dist/components/ui/assignee-dropdown.d.ts.map +1 -1
- package/dist/components/ui/assignee-dropdown.js +23 -23
- package/dist/components/ui/autocomplete.d.ts +0 -8
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/autocomplete.js +16 -20
- package/dist/components/ui/avatar-stack.d.ts +4 -8
- package/dist/components/ui/avatar-stack.d.ts.map +1 -1
- package/dist/components/ui/avatar-stack.js +13 -11
- package/dist/components/ui/badge.d.ts +5 -10
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/badge.js +3 -2
- package/dist/components/ui/breadcrumb.d.ts +2 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.js +5 -1
- package/dist/components/ui/button/button-styles.d.ts +21 -0
- package/dist/components/ui/button/button-styles.d.ts.map +1 -1
- package/dist/components/ui/button/button-styles.js +63 -0
- package/dist/components/ui/button/button.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/button/button.js +33 -3
- package/dist/components/ui/button/index.d.ts +1 -1
- package/dist/components/ui/button/index.d.ts.map +1 -1
- package/dist/components/ui/button/index.js +2 -1
- package/dist/components/ui/button/split-button.d.ts +2 -9
- package/dist/components/ui/button/split-button.d.ts.map +1 -1
- package/dist/components/ui/button/split-button.js +18 -91
- package/dist/components/ui/checkbox-block.d.ts +9 -1
- package/dist/components/ui/checkbox-block.d.ts.map +1 -1
- package/dist/components/ui/checkbox-block.js +110 -93
- package/dist/components/ui/checkbox-with-description.d.ts +5 -0
- package/dist/components/ui/checkbox-with-description.d.ts.map +1 -1
- package/dist/components/ui/checkbox-with-description.js +36 -36
- package/dist/components/ui/checkbox.d.ts +10 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -1
- package/dist/components/ui/checkbox.js +21 -14
- package/dist/components/ui/chevron-button.d.ts +2 -2
- package/dist/components/ui/chevron-button.d.ts.map +1 -1
- package/dist/components/ui/chevron-button.js +13 -20
- package/dist/components/ui/color-preset-select.d.ts +4 -1
- package/dist/components/ui/color-preset-select.d.ts.map +1 -1
- package/dist/components/ui/color-preset-select.js +24 -12
- package/dist/components/ui/color-swatch.d.ts +2 -1
- package/dist/components/ui/color-swatch.d.ts.map +1 -1
- package/dist/components/ui/color-swatch.js +5 -8
- package/dist/components/ui/content-loader.d.ts.map +1 -1
- package/dist/components/ui/content-loader.js +12 -9
- package/dist/components/ui/cursor-pagination.d.ts +5 -8
- package/dist/components/ui/cursor-pagination.d.ts.map +1 -1
- package/dist/components/ui/cursor-pagination.js +6 -8
- package/dist/components/ui/dashboard-info-card.d.ts +13 -7
- package/dist/components/ui/dashboard-info-card.d.ts.map +1 -1
- package/dist/components/ui/dashboard-info-card.js +19 -10
- package/dist/components/ui/data-attribution.d.ts +0 -6
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/data-attribution.js +0 -1
- package/dist/components/ui/data-table/data-table-body.d.ts +7 -1
- package/dist/components/ui/data-table/data-table-body.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-body.js +47 -15
- package/dist/components/ui/data-table/data-table-cursor-footer.d.ts +1 -1
- package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-header.js +5 -0
- package/dist/components/ui/data-table/data-table-infinite-footer.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-infinite-footer.js +7 -5
- package/dist/components/ui/data-table/data-table-reserved-rows.d.ts +27 -0
- package/dist/components/ui/data-table/data-table-reserved-rows.d.ts.map +1 -0
- package/dist/components/ui/data-table/data-table-reserved-rows.js +73 -0
- package/dist/components/ui/data-table/data-table-row-classes.d.ts +34 -0
- package/dist/components/ui/data-table/data-table-row-classes.d.ts.map +1 -0
- package/dist/components/ui/data-table/data-table-row-classes.js +20 -0
- package/dist/components/ui/data-table/data-table-row-count.d.ts +3 -6
- package/dist/components/ui/data-table/data-table-row-count.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row-count.js +3 -3
- package/dist/components/ui/data-table/data-table-row.d.ts +18 -38
- package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row.js +109 -9
- package/dist/components/ui/data-table/index.d.ts +6 -9
- package/dist/components/ui/data-table/index.d.ts.map +1 -1
- package/dist/components/ui/data-table/index.js +12 -7
- package/dist/components/ui/data-table/types.d.ts +13 -0
- package/dist/components/ui/data-table/types.d.ts.map +1 -1
- package/dist/components/ui/data-table/utils.d.ts +8 -0
- package/dist/components/ui/data-table/utils.d.ts.map +1 -1
- package/dist/components/ui/data-table/utils.js +6 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/date-picker.js +12 -47
- package/dist/components/ui/device-card.d.ts +7 -1
- package/dist/components/ui/device-card.d.ts.map +1 -1
- package/dist/components/ui/device-card.js +40 -27
- package/dist/components/ui/entity-image.d.ts +0 -2
- package/dist/components/ui/entity-image.d.ts.map +1 -1
- package/dist/components/ui/entity-image.js +2 -6
- package/dist/components/ui/feature-card.d.ts +0 -5
- package/dist/components/ui/feature-card.d.ts.map +1 -1
- package/dist/components/ui/feature-card.js +12 -9
- package/dist/components/ui/feature-list.d.ts +0 -10
- package/dist/components/ui/feature-list.d.ts.map +1 -1
- package/dist/components/ui/feature-list.js +3 -4
- package/dist/components/ui/field-value-skeleton.d.ts +7 -0
- package/dist/components/ui/field-value-skeleton.d.ts.map +1 -0
- package/dist/components/ui/field-value-skeleton.js +10 -0
- package/dist/components/ui/file-manager/file-manager-breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/file-manager/file-manager-breadcrumb.js +8 -6
- package/dist/components/ui/file-manager/file-manager-table-row.d.ts +12 -1
- package/dist/components/ui/file-manager/file-manager-table-row.d.ts.map +1 -1
- package/dist/components/ui/file-manager/file-manager-table-row.js +100 -31
- package/dist/components/ui/file-manager/file-manager-table.d.ts +1 -1
- package/dist/components/ui/file-manager/file-manager-table.d.ts.map +1 -1
- package/dist/components/ui/file-manager/file-manager-table.js +41 -39
- package/dist/components/ui/file-manager/file-manager.d.ts +1 -1
- package/dist/components/ui/file-manager/file-manager.d.ts.map +1 -1
- package/dist/components/ui/file-manager/file-manager.js +62 -44
- package/dist/components/ui/file-manager/index.cjs +240 -217
- package/dist/components/ui/file-manager/index.d.ts +0 -1
- package/dist/components/ui/file-manager/index.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.js +0 -2
- package/dist/components/ui/file-manager/types.d.ts +12 -1
- package/dist/components/ui/file-manager/types.d.ts.map +1 -1
- package/dist/components/ui/file-upload.d.ts.map +1 -1
- package/dist/components/ui/file-upload.js +38 -26
- package/dist/components/ui/filter-checkbox-item.d.ts +1 -10
- package/dist/components/ui/filter-checkbox-item.d.ts.map +1 -1
- package/dist/components/ui/filter-checkbox-item.js +11 -7
- package/dist/components/ui/filter-modal.d.ts +3 -3
- package/dist/components/ui/filter-modal.d.ts.map +1 -1
- package/dist/components/ui/filter-modal.js +74 -67
- package/dist/components/ui/filter-pill-row.d.ts +0 -13
- package/dist/components/ui/filter-pill-row.d.ts.map +1 -1
- package/dist/components/ui/filter-pill-row.js +3 -3
- package/dist/components/ui/hidden-tags-popup.d.ts.map +1 -1
- package/dist/components/ui/hidden-tags-popup.js +6 -3
- package/dist/components/ui/hover-dropdown.d.ts.map +1 -1
- package/dist/components/ui/hover-dropdown.js +7 -3
- package/dist/components/ui/image-uploader.d.ts.map +1 -1
- package/dist/components/ui/image-uploader.js +7 -12
- package/dist/components/ui/index.cjs +35 -160
- package/dist/components/ui/index.d.ts +5 -6
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -23
- package/dist/components/ui/info-card-row.d.ts +0 -12
- package/dist/components/ui/info-card-row.d.ts.map +1 -1
- package/dist/components/ui/info-card-row.js +1 -2
- package/dist/components/ui/info-card.d.ts +32 -21
- package/dist/components/ui/info-card.d.ts.map +1 -1
- package/dist/components/ui/info-card.js +91 -76
- package/dist/components/ui/info-section.d.ts +8 -1
- package/dist/components/ui/info-section.d.ts.map +1 -1
- package/dist/components/ui/info-section.js +39 -18
- package/dist/components/ui/input-styles.d.ts +3 -0
- package/dist/components/ui/input-styles.d.ts.map +1 -1
- package/dist/components/ui/input-styles.js +2 -0
- package/dist/components/ui/input-trigger.d.ts +3 -0
- package/dist/components/ui/input-trigger.d.ts.map +1 -1
- package/dist/components/ui/input-trigger.js +13 -21
- package/dist/components/ui/input.d.ts +7 -1
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/input.js +14 -4
- package/dist/components/ui/label.d.ts +2 -2
- package/dist/components/ui/label.d.ts.map +1 -1
- package/dist/components/ui/label.js +8 -5
- package/dist/components/ui/leader-row.d.ts +17 -13
- package/dist/components/ui/leader-row.d.ts.map +1 -1
- package/dist/components/ui/leader-row.js +34 -25
- package/dist/components/ui/loading-scope.d.ts +82 -3
- package/dist/components/ui/loading-scope.d.ts.map +1 -1
- package/dist/components/ui/loading-scope.js +56 -4
- package/dist/components/ui/markdown/engine.d.ts +5 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -1
- package/dist/components/ui/markdown/engine.js +10 -6
- package/dist/components/ui/markdown/index.d.ts +1 -1
- package/dist/components/ui/markdown/index.d.ts.map +1 -1
- package/dist/components/ui/markdown/index.js +1 -5
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +0 -10
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -1
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.js +2 -10
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/modal-v2.js +7 -3
- package/dist/components/ui/numbered-step.d.ts +9 -11
- package/dist/components/ui/numbered-step.d.ts.map +1 -1
- package/dist/components/ui/numbered-step.js +12 -2
- package/dist/components/ui/organization-card.d.ts +14 -1
- package/dist/components/ui/organization-card.d.ts.map +1 -1
- package/dist/components/ui/organization-card.js +70 -29
- package/dist/components/ui/page-actions.d.ts +5 -5
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/components/ui/page-actions.js +45 -50
- package/dist/components/ui/password-input.d.ts.map +1 -1
- package/dist/components/ui/password-input.js +5 -3
- package/dist/components/ui/people-cell.d.ts +3 -5
- package/dist/components/ui/people-cell.d.ts.map +1 -1
- package/dist/components/ui/people-cell.js +6 -6
- package/dist/components/ui/product-screen-frame.d.ts.map +1 -1
- package/dist/components/ui/product-screen-frame.js +19 -6
- package/dist/components/ui/progress-bar.d.ts +3 -4
- package/dist/components/ui/progress-bar.d.ts.map +1 -1
- package/dist/components/ui/progress-bar.js +7 -4
- package/dist/components/ui/query-report-table/index.d.ts +1 -2
- package/dist/components/ui/query-report-table/index.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/index.js +0 -2
- package/dist/components/ui/query-report-table/query-report-table-header.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/query-report-table-header.js +3 -2
- package/dist/components/ui/query-report-table/query-report-table-row.d.ts +15 -1
- package/dist/components/ui/query-report-table/query-report-table-row.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/query-report-table-row.js +30 -21
- package/dist/components/ui/query-report-table/query-report-table-skeleton.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/query-report-table-skeleton.js +20 -28
- package/dist/components/ui/query-report-table/query-report-table.d.ts +1 -1
- package/dist/components/ui/query-report-table/query-report-table.d.ts.map +1 -1
- package/dist/components/ui/query-report-table/query-report-table.js +19 -14
- package/dist/components/ui/query-report-table/types.d.ts +5 -1
- package/dist/components/ui/query-report-table/types.d.ts.map +1 -1
- package/dist/components/ui/radio-group.d.ts +14 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group.js +51 -33
- package/dist/components/ui/release-changelog-section.d.ts +0 -19
- package/dist/components/ui/release-changelog-section.d.ts.map +1 -1
- package/dist/components/ui/release-changelog-section.js +11 -16
- package/dist/components/ui/search-input.d.ts.map +1 -1
- package/dist/components/ui/search-input.js +11 -14
- package/dist/components/ui/searchable-select.d.ts.map +1 -1
- package/dist/components/ui/searchable-select.js +30 -33
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/select.js +4 -1
- package/dist/components/ui/service-card.d.ts +9 -1
- package/dist/components/ui/service-card.d.ts.map +1 -1
- package/dist/components/ui/service-card.js +89 -66
- package/dist/components/ui/slack-channel-chip.d.ts +0 -4
- package/dist/components/ui/slack-channel-chip.d.ts.map +1 -1
- package/dist/components/ui/slack-channel-chip.js +1 -2
- package/dist/components/ui/snap-carousel.d.ts +38 -6
- package/dist/components/ui/snap-carousel.d.ts.map +1 -1
- package/dist/components/ui/snap-carousel.js +122 -63
- package/dist/components/ui/square-avatar.d.ts +3 -2
- package/dist/components/ui/square-avatar.d.ts.map +1 -1
- package/dist/components/ui/square-avatar.js +18 -6
- package/dist/components/ui/stacked-rows-panel.d.ts +21 -23
- package/dist/components/ui/stacked-rows-panel.d.ts.map +1 -1
- package/dist/components/ui/stacked-rows-panel.js +36 -38
- package/dist/components/ui/status-badge.d.ts +9 -11
- package/dist/components/ui/status-badge.d.ts.map +1 -1
- package/dist/components/ui/status-badge.js +14 -3
- package/dist/components/ui/status-indicator.d.ts +0 -4
- package/dist/components/ui/status-indicator.d.ts.map +1 -1
- package/dist/components/ui/status-indicator.js +1 -2
- package/dist/components/ui/strip-card.d.ts +0 -9
- package/dist/components/ui/strip-card.d.ts.map +1 -1
- package/dist/components/ui/strip-card.js +1 -2
- package/dist/components/ui/super-user-banner-view.d.ts +4 -1
- package/dist/components/ui/super-user-banner-view.d.ts.map +1 -1
- package/dist/components/ui/super-user-banner-view.js +10 -2
- package/dist/components/ui/switch.d.ts +3 -0
- package/dist/components/ui/switch.d.ts.map +1 -1
- package/dist/components/ui/switch.js +15 -10
- package/dist/components/ui/tab-navigation.d.ts +11 -17
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.js +28 -33
- package/dist/components/ui/tab-selector.d.ts.map +1 -1
- package/dist/components/ui/tab-selector.js +7 -11
- package/dist/components/ui/tabs.d.ts +1 -22
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/ui/tabs.js +1 -16
- package/dist/components/ui/tag-search-input.d.ts.map +1 -1
- package/dist/components/ui/tag-search-input.js +9 -10
- package/dist/components/ui/tag-select-dropdown.d.ts.map +1 -1
- package/dist/components/ui/tag-select-dropdown.js +14 -26
- package/dist/components/ui/tag.d.ts +6 -10
- package/dist/components/ui/tag.d.ts.map +1 -1
- package/dist/components/ui/tag.js +23 -9
- package/dist/components/ui/tags-input.d.ts +5 -7
- package/dist/components/ui/tags-input.d.ts.map +1 -1
- package/dist/components/ui/tags-input.js +14 -11
- package/dist/components/ui/tags-manager.d.ts.map +1 -1
- package/dist/components/ui/tags-manager.js +8 -3
- package/dist/components/ui/test-run.d.ts +1 -8
- package/dist/components/ui/test-run.d.ts.map +1 -1
- package/dist/components/ui/test-run.js +17 -11
- package/dist/components/ui/textarea.d.ts +1 -10
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/textarea.js +38 -35
- package/dist/components/ui/ticket-attachments-list.d.ts.map +1 -1
- package/dist/components/ui/ticket-attachments-list.js +11 -6
- package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
- package/dist/components/ui/ticket-info-section.js +13 -18
- package/dist/components/ui/ticket-note-card.d.ts.map +1 -1
- package/dist/components/ui/ticket-note-card.js +19 -12
- package/dist/components/ui/ticket-notes-section.d.ts.map +1 -1
- package/dist/components/ui/ticket-notes-section.js +6 -8
- package/dist/components/ui/ticket-status-config-row.d.ts +6 -1
- package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
- package/dist/components/ui/ticket-status-config-row.js +41 -23
- package/dist/components/ui/ticket-status-tag.d.ts +3 -1
- package/dist/components/ui/ticket-status-tag.d.ts.map +1 -1
- package/dist/components/ui/ticket-status-tag.js +42 -28
- package/dist/components/ui/timezone-select.d.ts +3 -2
- package/dist/components/ui/timezone-select.d.ts.map +1 -1
- package/dist/components/ui/timezone-select.js +3 -1
- package/dist/components/ui/toaster.d.ts.map +1 -1
- package/dist/components/ui/toaster.js +18 -20
- package/dist/components/ui/truncate-text.d.ts +6 -1
- package/dist/components/ui/truncate-text.d.ts.map +1 -1
- package/dist/components/ui/truncate-text.js +7 -1
- package/dist/components/user-display.d.ts +0 -11
- package/dist/components/user-display.d.ts.map +1 -1
- package/dist/components/user-display.js +3 -4
- package/dist/components/vendor-display-button.d.ts +4 -4
- package/dist/components/vendor-display-button.d.ts.map +1 -1
- package/dist/components/vendor-display-button.js +21 -34
- package/dist/components/vendor-icon.d.ts +3 -6
- package/dist/components/vendor-icon.d.ts.map +1 -1
- package/dist/components/vendor-icon.js +21 -9
- package/dist/components/vendor-tag.d.ts +0 -5
- package/dist/components/vendor-tag.d.ts.map +1 -1
- package/dist/components/vendor-tag.js +4 -2
- package/dist/{devices-WL3I23EE.cjs → devices-TDKCKWRP.cjs} +33 -34
- package/dist/embed-shims/index.cjs +4 -4
- package/dist/embed-shims/next-image.cjs +4 -4
- package/dist/embed-shims/next-image.d.ts +1 -1
- package/dist/{embeddable-chat-KEWUGQIF.cjs → embeddable-chat-HEL4Z2FL.cjs} +185 -183
- package/dist/hooks/index.cjs +11 -9
- package/dist/hooks/use-toast.d.ts +1 -1
- package/dist/index.cjs +65 -226
- package/dist/{knowledge-O3PXUX73.cjs → knowledge-A5KJ4CAD.cjs} +33 -34
- package/dist/loading-standard/engine.d.mts +131 -0
- package/dist/loading-standard/engine.d.mts.map +1 -0
- package/dist/loading-standard/rule-examples.d.mts +13 -0
- package/dist/loading-standard/rule-examples.d.mts.map +1 -0
- package/dist/{logs-XODCXWYI.cjs → logs-6WRBPIMM.cjs} +41 -42
- package/dist/{policies-CQACBIKK.cjs → policies-ICRXHW3H.cjs} +36 -37
- package/dist/{remote-session-TGM2OTTV.cjs → remote-session-MEV5JL2Z.cjs} +17 -17
- package/dist/{scripts-QMM6SEB5.cjs → scripts-AZVCZE6C.cjs} +39 -40
- package/dist/{software-update-CCCKS4ZE.cjs → software-update-4ZBCGJGA.cjs} +36 -37
- package/dist/{tickets-board-FB5AGLJD.cjs → tickets-board-BJPQD3HE.cjs} +17 -17
- package/dist/types/logs.types.d.ts +9 -3
- package/dist/types/logs.types.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +10 -0
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/{video-file-player-SIJW5UWU.cjs → video-file-player-DETXW4OJ.cjs} +14 -14
- package/package.json +1 -1
- package/src/components/agent-mark.tsx +9 -2
- package/src/components/authors/author-detail-view.tsx +5 -5
- package/src/components/case-studies/entity-quotes-panel.tsx +0 -34
- package/src/components/case-studies/index.ts +1 -2
- package/src/components/case-studies/quote-cards-strip.tsx +15 -12
- package/src/components/categories-cart.tsx +23 -8
- package/src/components/category-card.tsx +80 -14
- package/src/components/chat/approval-batch-message.tsx +2 -6
- package/src/components/chat/ask-display.tsx +12 -13
- package/src/components/chat/ask-prompts.tsx +26 -22
- package/src/components/chat/chat-archive-page.tsx +33 -30
- package/src/components/chat/chat-container.tsx +39 -41
- package/src/components/chat/chat-context-memory-bar.tsx +12 -11
- package/src/components/chat/chat-context-picker.tsx +50 -41
- package/src/components/chat/chat-input.tsx +6 -14
- package/src/components/chat/chat-message-enhanced.tsx +36 -18
- package/src/components/chat/chat-message-layout.ts +39 -0
- package/src/components/chat/chat-message-list.tsx +35 -18
- package/src/components/chat/chat-message-row.tsx +57 -25
- package/src/components/chat/chat-message-skeleton.tsx +109 -56
- package/src/components/chat/chat-nav-sidebar.tsx +116 -39
- package/src/components/chat/chat-panel-header.tsx +21 -15
- package/src/components/chat/chat-sidebar.tsx +80 -48
- package/src/components/chat/chat-ticket-list.tsx +32 -23
- package/src/components/chat/chat-top-navigation.tsx +57 -49
- package/src/components/chat/chat-turn.tsx +20 -17
- package/src/components/chat/context-items-list.tsx +64 -33
- package/src/components/chat/embeddable-chat.tsx +133 -138
- package/src/components/chat/entity-cards/admin-content-card-grid.tsx +64 -34
- package/src/components/chat/entity-cards/admin-content-card.tsx +14 -8
- package/src/components/chat/entity-cards/ai-prompt-card.tsx +9 -11
- package/src/components/chat/entity-cards/blog-card.tsx +28 -7
- package/src/components/chat/entity-cards/campaign-card-admin.tsx +1 -1
- package/src/components/chat/entity-cards/case-study-card.tsx +10 -3
- package/src/components/chat/entity-cards/chat-ticket-item.tsx +86 -79
- package/src/components/chat/entity-cards/customer-interview-card.tsx +47 -13
- package/src/components/chat/entity-cards/data-room-doc-card.tsx +1 -1
- package/src/components/chat/entity-cards/design-doc-card.tsx +6 -6
- package/src/components/chat/entity-cards/dispatch.tsx +25 -30
- package/src/components/chat/entity-cards/employee-entry-card.tsx +30 -15
- package/src/components/chat/entity-cards/entity-author-card.tsx +16 -8
- package/src/components/chat/entity-cards/entity-portrait-card.tsx +12 -3
- package/src/components/chat/entity-cards/generic-entity-card.tsx +1 -1
- package/src/components/chat/entity-cards/github-activity-card.tsx +1 -1
- package/src/components/chat/entity-cards/how-i-work-card.tsx +9 -12
- package/src/components/chat/entity-cards/hubspot-ticket-card.tsx +1 -1
- package/src/components/chat/entity-cards/index.ts +21 -61
- package/src/components/chat/entity-cards/investor-update-card.tsx +30 -11
- package/src/components/chat/entity-cards/onboarding-guide-card.tsx +45 -13
- package/src/components/chat/entity-cards/program-card.tsx +69 -41
- package/src/components/chat/entity-cards/roadmap-card.tsx +27 -10
- package/src/components/chat/entity-cards/slack-message-card.tsx +1 -1
- package/src/components/chat/entity-cards/what-i-shipped-card.tsx +9 -7
- package/src/components/chat/error-message-display.tsx +4 -6
- package/src/components/chat/guide-mode-banner.tsx +4 -7
- package/src/components/chat/guide-welcome.tsx +9 -6
- package/src/components/chat/hooks/use-chat-dialog-manager.ts +2 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +2 -1
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +2 -1
- package/src/components/chat/index.ts +1 -4
- package/src/components/chat/mingo-archive-list.tsx +14 -8
- package/src/components/chat/mingo-chat-header.tsx +14 -13
- package/src/components/chat/mingo-chat-history.tsx +122 -78
- package/src/components/chat/mingo-chat-modals.tsx +18 -22
- package/src/components/chat/mingo-chat-rail.tsx +16 -9
- package/src/components/chat/mingo-history-rail.tsx +12 -6
- package/src/components/chat/mingo-info-card.tsx +26 -18
- package/src/components/chat/mingo-onboarding-card.tsx +117 -36
- package/src/components/chat/mingo-welcome.tsx +22 -7
- package/src/components/chat/model-display.tsx +29 -33
- package/src/components/chat/msp-organization-card.tsx +64 -46
- package/src/components/chat/quick-action-chip.tsx +52 -7
- package/src/components/chat/quick-action-wall.tsx +29 -13
- package/src/components/chat/tool-execution-display.tsx +3 -6
- package/src/components/chat/types/component.types.ts +26 -0
- package/src/components/chat/types/entities/program-types.ts +3 -0
- package/src/components/chat/utils/compact-card-skeleton.tsx +3 -3
- package/src/components/comment-card.tsx +79 -49
- package/src/components/comment-thread.tsx +56 -47
- package/src/components/docs/docs-hub-page.tsx +3 -7
- package/src/components/docs/index.ts +0 -2
- package/src/components/embeds/embed-viewer-frame.tsx +4 -1
- package/src/components/embeds/index.ts +1 -1
- package/src/components/embeds/og-link-preview.tsx +2 -2
- package/src/components/faq/faq-section.tsx +20 -12
- package/src/components/faq/index.ts +0 -2
- package/src/components/faq-accordion.tsx +33 -23
- package/src/components/features/ai-enrich/AIEnrichSection.tsx +2 -3
- package/src/components/features/approval-level.tsx +2 -2
- package/src/components/features/auth/back-to-login-link.tsx +3 -3
- package/src/components/features/auth/complete-account-form.tsx +8 -3
- package/src/components/features/auth/create-organization-form.tsx +27 -6
- package/src/components/features/auth/invite-link-invalid-modal.tsx +6 -4
- package/src/components/features/auth/login-form.tsx +9 -2
- package/src/components/features/auth/sso-join-form.tsx +35 -7
- package/src/components/features/auth/sso-providers.tsx +30 -1
- package/src/components/features/board/board-column.tsx +9 -17
- package/src/components/features/board/board-ticket-approval.tsx +4 -8
- package/src/components/features/board/board.tsx +41 -11
- package/src/components/features/board/index.ts +1 -3
- package/src/components/features/board/ticket-card.tsx +60 -8
- package/src/components/features/board/types.ts +6 -0
- package/src/components/features/capability-explorer.tsx +88 -71
- package/src/components/features/card-hit-layer.tsx +4 -2
- package/src/components/features/cloud-tenants/tenant-access-cell.tsx +5 -2
- package/src/components/features/cloud-tenants/tenant-cell.tsx +10 -2
- package/src/components/features/cloud-tenants/tenant-customer-cell.tsx +4 -1
- package/src/components/features/cloud-tenants/tenants-table-view.tsx +44 -16
- package/src/components/features/command-box.tsx +25 -10
- package/src/components/features/conversation-card.tsx +101 -48
- package/src/components/features/devices/device-selector/device-selector.tsx +135 -105
- package/src/components/features/devices/device-selector/device-selector.types.ts +5 -0
- package/src/components/features/devices/device-type-tile.tsx +11 -1
- package/src/components/features/devices/devices-grid-filters.tsx +5 -15
- package/src/components/features/devices/devices-grid.tsx +40 -10
- package/src/components/features/devices/devices-list.tsx +9 -2
- package/src/components/features/devices/devices-panel-view.tsx +17 -4
- package/src/components/features/devices/devices-table-columns.tsx +42 -8
- package/src/components/features/devices/index.ts +0 -1
- package/src/components/features/entity-tag-badges.tsx +24 -37
- package/src/components/features/entity-video-section.tsx +5 -5
- package/src/components/features/filters-dropdown.tsx +17 -22
- package/src/components/features/highlight-video-section.tsx +7 -5
- package/src/components/features/knowledge-base/knowledge-base-table.tsx +101 -60
- package/src/components/features/logs/index.ts +2 -2
- package/src/components/features/logs/logs-page-view.tsx +1 -0
- package/src/components/features/logs/logs-table-columns.ts +2 -3
- package/src/components/features/logs/logs-table-view.tsx +98 -135
- package/src/components/features/model-token-rates.tsx +31 -28
- package/src/components/features/notifications/index.ts +2 -2
- package/src/components/features/notifications/notification-drawer.tsx +7 -4
- package/src/components/features/notifications/notification-tile.tsx +53 -12
- package/src/components/features/os-type-badge-group.tsx +19 -1
- package/src/components/features/os-type-badge.tsx +13 -0
- package/src/components/features/paths-display.tsx +26 -20
- package/src/components/features/platform-badge.tsx +7 -3
- package/src/components/features/policies/policies-table.tsx +62 -27
- package/src/components/features/policy-configuration-panel.tsx +94 -32
- package/src/components/features/push-button-selector.tsx +21 -10
- package/src/components/features/remote-session/index.ts +1 -1
- package/src/components/features/remote-session/remote-desktop-chat-message-row.tsx +41 -25
- package/src/components/features/remote-session/remote-desktop-chat-panel.tsx +21 -7
- package/src/components/features/remote-session/remote-desktop-fullscreen-toolbar.tsx +5 -13
- package/src/components/features/remote-session/remote-desktop-view.tsx +13 -15
- package/src/components/features/remote-session/remote-session-block.tsx +34 -36
- package/src/components/features/remote-session/remote-session-chat-panel.tsx +6 -4
- package/src/components/features/remote-session/remote-session-events.tsx +12 -10
- package/src/components/features/remote-session/remote-session-overlay.tsx +4 -7
- package/src/components/features/scripts/execution-cells.tsx +18 -4
- package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +5 -5
- package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +6 -3
- package/src/components/features/scripts/script-executions-table.tsx +29 -7
- package/src/components/features/scripts/script-schedules-table.tsx +42 -9
- package/src/components/features/scripts/script-shell-badge.tsx +11 -0
- package/src/components/features/scripts/scripts-table-shared.tsx +8 -2
- package/src/components/features/scripts/scripts-table.tsx +28 -7
- package/src/components/features/select-button.tsx +7 -15
- package/src/components/features/status-filter-component.tsx +1 -1
- package/src/components/features/tags-selector.tsx +5 -4
- package/src/components/features/ticket-status-config-list/ticket-status-config-list.tsx +52 -5
- package/src/components/features/video-bites-strip.tsx +4 -9
- package/src/components/features/video-hover-preview.tsx +37 -46
- package/src/components/features/video-shared.tsx +3 -0
- package/src/components/features/video-source-selector.tsx +6 -4
- package/src/components/features/video.tsx +7 -65
- package/src/components/features/walkthrough-video.tsx +1 -1
- package/src/components/filter-chip.tsx +6 -16
- package/src/components/help-center-pages/download-apps-page.tsx +21 -13
- package/src/components/help-center-pages/index.ts +1 -1
- package/src/components/help-center-pages/trust-center-page.tsx +5 -25
- package/src/components/help-center-pages/trust-center-sections.tsx +14 -11
- package/src/components/index.ts +1 -9
- package/src/components/interactive-wrapper.tsx +0 -21
- package/src/components/layout/back-button.tsx +15 -18
- package/src/components/layout/list-page-layout.tsx +4 -3
- package/src/components/layout/page-container.tsx +2 -2
- package/src/components/layout/page-heading.tsx +26 -2
- package/src/components/loading/index.ts +4 -34
- package/src/components/loading/loading-fallback.tsx +81 -0
- package/src/components/loading/loading-region.tsx +2 -6
- package/src/components/log-severity-dot.tsx +7 -2
- package/src/components/logs-list.tsx +6 -8
- package/src/components/meeting-scheduler/booking-form.tsx +5 -5
- package/src/components/meeting-scheduler/context-panel.tsx +13 -47
- package/src/components/meeting-scheduler/directory.tsx +6 -7
- package/src/components/meeting-scheduler/host-identity.tsx +68 -0
- package/src/components/meeting-scheduler/index.tsx +5 -9
- package/src/components/meeting-scheduler/modal-header.tsx +8 -50
- package/src/components/meeting-scheduler/slot-picker.tsx +13 -16
- package/src/components/metric-value.tsx +1 -1
- package/src/components/msp-display.tsx +3 -3
- package/src/components/navigation/app-header.tsx +4 -2
- package/src/components/navigation/header-button.tsx +6 -10
- package/src/components/navigation/header-mingo-button.tsx +5 -9
- package/src/components/navigation/header-organization-filter.tsx +3 -11
- package/src/components/navigation/mobile-burger-menu.tsx +7 -24
- package/src/components/navigation/multi-level-navigation.tsx +33 -44
- package/src/components/navigation/navigation-sidebar-item.tsx +47 -50
- package/src/components/navigation/navigation-sidebar-toggle.tsx +6 -9
- package/src/components/navigation/navigation-sidebar.tsx +7 -3
- package/src/components/navigation/sticky-section-nav.tsx +12 -16
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +3 -11
- package/src/components/onboarding-guides/onboarding-guides-catalog-view.tsx +52 -18
- package/src/components/pagination.tsx +1 -10
- package/src/components/platform/ToolBadge.tsx +2 -2
- package/src/components/platform/index.ts +2 -2
- package/src/components/product-demo/product-screen.tsx +36 -16
- package/src/components/product-demo/screen-loading.tsx +2 -6
- package/src/components/related-content/card-registry.tsx +23 -35
- package/src/components/related-content/index.ts +0 -2
- package/src/components/related-content/related-content-section.tsx +2 -2
- package/src/components/results-count.tsx +1 -1
- package/src/components/shared/article-author-byline.tsx +1 -1
- package/src/components/shared/delivery/delivery-row.tsx +14 -9
- package/src/components/shared/delivery/delivery-table.tsx +14 -11
- package/src/components/shared/dev-section/dev-card-row.tsx +22 -59
- package/src/components/shared/dev-section/index.ts +1 -6
- package/src/components/shared/legal-document/index.ts +0 -1
- package/src/components/shared/legal-document/legal-document-page.tsx +4 -4
- package/src/components/shared/media-gallery-strip.tsx +28 -20
- package/src/components/shared/onboarding/onboarding-walkthrough.tsx +4 -1
- package/src/components/shared/product-release/index.ts +0 -1
- package/src/components/shared/product-release/product-release-card-skeleton.tsx +4 -4
- package/src/components/shared/product-release/product-release-card.tsx +16 -8
- package/src/components/shared/product-release/product-releases-view.tsx +27 -22
- package/src/components/shared/product-release/release-detail-page.tsx +3 -11
- package/src/components/shared/roadmap/index.ts +0 -1
- package/src/components/shared/roadmap/roadmap-grid.tsx +56 -7
- package/src/components/shared/roadmap/roadmap-view.tsx +5 -3
- package/src/components/smooth-accordion.tsx +0 -2
- package/src/components/social-icon-row.tsx +9 -0
- package/src/components/tickets/help-center-card.tsx +62 -14
- package/src/components/tickets/help-center-create-form.tsx +29 -112
- package/src/components/tickets/help-center-list.tsx +26 -24
- package/src/components/tickets/index.ts +1 -5
- package/src/components/tickets/ticket-center.tsx +16 -27
- package/src/components/tickets/ticket-detail-drawer.tsx +8 -5
- package/src/components/tickets/ticket-open-form.tsx +5 -4
- package/src/components/tickets/ticket-row.tsx +42 -5
- package/src/components/ui/actions-menu.tsx +45 -44
- package/src/components/ui/allowed-domains-input.tsx +7 -8
- package/src/components/ui/assignee-dropdown.tsx +26 -24
- package/src/components/ui/autocomplete.tsx +26 -25
- package/src/components/ui/avatar-stack.tsx +24 -15
- package/src/components/ui/badge.tsx +9 -3
- package/src/components/ui/breadcrumb.tsx +8 -1
- package/src/components/ui/button/button-styles.ts +79 -0
- package/src/components/ui/button/button.tsx +36 -2
- package/src/components/ui/button/index.ts +1 -1
- package/src/components/ui/button/split-button.tsx +21 -102
- package/src/components/ui/checkbox-block.tsx +118 -88
- package/src/components/ui/checkbox-with-description.tsx +39 -33
- package/src/components/ui/checkbox.tsx +34 -16
- package/src/components/ui/chevron-button.tsx +16 -23
- package/src/components/ui/color-preset-select.tsx +34 -11
- package/src/components/ui/color-swatch.tsx +9 -8
- package/src/components/ui/content-loader.tsx +42 -29
- package/src/components/ui/cursor-pagination.tsx +10 -24
- package/src/components/ui/dashboard-info-card.tsx +35 -16
- package/src/components/ui/data-attribution.tsx +1 -1
- package/src/components/ui/data-table/data-table-body.tsx +62 -15
- package/src/components/ui/data-table/data-table-cursor-footer.tsx +1 -1
- package/src/components/ui/data-table/data-table-header.tsx +13 -1
- package/src/components/ui/data-table/data-table-infinite-footer.tsx +10 -5
- package/src/components/ui/data-table/data-table-reserved-rows.tsx +111 -0
- package/src/components/ui/data-table/data-table-row-classes.ts +48 -0
- package/src/components/ui/data-table/data-table-row-count.tsx +5 -2
- package/src/components/ui/data-table/data-table-row.tsx +136 -10
- package/src/components/ui/data-table/index.ts +10 -9
- package/src/components/ui/data-table/types.ts +14 -0
- package/src/components/ui/data-table/utils.ts +12 -0
- package/src/components/ui/date-picker.tsx +23 -55
- package/src/components/ui/device-card.tsx +99 -43
- package/src/components/ui/entity-image.tsx +1 -4
- package/src/components/ui/feature-card.tsx +15 -8
- package/src/components/ui/feature-list.tsx +3 -3
- package/src/components/ui/field-value-skeleton.tsx +11 -0
- package/src/components/ui/file-manager/file-manager-breadcrumb.tsx +9 -7
- package/src/components/ui/file-manager/file-manager-table-row.tsx +126 -32
- package/src/components/ui/file-manager/file-manager-table.tsx +69 -96
- package/src/components/ui/file-manager/file-manager.tsx +19 -5
- package/src/components/ui/file-manager/index.ts +0 -1
- package/src/components/ui/file-manager/types.ts +12 -1
- package/src/components/ui/file-upload.tsx +40 -29
- package/src/components/ui/filter-checkbox-item.tsx +9 -3
- package/src/components/ui/filter-modal.tsx +66 -51
- package/src/components/ui/filter-pill-row.tsx +5 -2
- package/src/components/ui/hidden-tags-popup.tsx +7 -4
- package/src/components/ui/hover-dropdown.tsx +9 -5
- package/src/components/ui/image-uploader.tsx +8 -13
- package/src/components/ui/index.ts +4 -31
- package/src/components/ui/info-card-row.tsx +1 -1
- package/src/components/ui/info-card.tsx +193 -134
- package/src/components/ui/info-section.tsx +61 -24
- package/src/components/ui/input-styles.ts +3 -0
- package/src/components/ui/input-trigger.tsx +26 -26
- package/src/components/ui/input.tsx +24 -6
- package/src/components/ui/label.tsx +13 -8
- package/src/components/ui/leader-row.tsx +61 -46
- package/src/components/ui/loading-scope.tsx +147 -4
- package/src/components/ui/markdown/engine.tsx +21 -12
- package/src/components/ui/markdown/index.ts +1 -5
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +1 -18
- package/src/components/ui/modal-v2.tsx +8 -4
- package/src/components/ui/numbered-step.tsx +18 -1
- package/src/components/ui/organization-card.tsx +115 -32
- package/src/components/ui/page-actions.tsx +82 -127
- package/src/components/ui/password-input.tsx +6 -4
- package/src/components/ui/people-cell.tsx +8 -5
- package/src/components/ui/product-screen-frame.tsx +20 -10
- package/src/components/ui/progress-bar.tsx +12 -2
- package/src/components/ui/query-report-table/index.ts +0 -2
- package/src/components/ui/query-report-table/query-report-table-header.tsx +3 -2
- package/src/components/ui/query-report-table/query-report-table-row.tsx +31 -12
- package/src/components/ui/query-report-table/query-report-table-skeleton.tsx +41 -34
- package/src/components/ui/query-report-table/query-report-table.tsx +42 -26
- package/src/components/ui/query-report-table/types.ts +5 -1
- package/src/components/ui/radio-group.tsx +111 -50
- package/src/components/ui/release-changelog-section.tsx +23 -24
- package/src/components/ui/search-input.tsx +19 -18
- package/src/components/ui/searchable-select.tsx +36 -39
- package/src/components/ui/select.tsx +6 -1
- package/src/components/ui/service-card.tsx +118 -55
- package/src/components/ui/slack-channel-chip.tsx +1 -1
- package/src/components/ui/snap-carousel.tsx +146 -61
- package/src/components/ui/square-avatar.tsx +25 -6
- package/src/components/ui/stacked-rows-panel.tsx +94 -70
- package/src/components/ui/status-badge.tsx +21 -3
- package/src/components/ui/status-indicator.tsx +1 -1
- package/src/components/ui/strip-card.tsx +1 -1
- package/src/components/ui/super-user-banner-view.tsx +18 -2
- package/src/components/ui/switch.tsx +25 -13
- package/src/components/ui/tab-navigation.tsx +63 -55
- package/src/components/ui/tab-selector.tsx +10 -20
- package/src/components/ui/tabs.tsx +1 -30
- package/src/components/ui/tag-search-input.tsx +11 -12
- package/src/components/ui/tag-select-dropdown.tsx +9 -17
- package/src/components/ui/tag.tsx +32 -10
- package/src/components/ui/tags-input.tsx +19 -11
- package/src/components/ui/tags-manager.tsx +10 -5
- package/src/components/ui/test-run.tsx +40 -28
- package/src/components/ui/textarea.tsx +27 -20
- package/src/components/ui/ticket-attachments-list.tsx +13 -8
- package/src/components/ui/ticket-info-section.tsx +15 -17
- package/src/components/ui/ticket-note-card.tsx +23 -16
- package/src/components/ui/ticket-notes-section.tsx +7 -9
- package/src/components/ui/ticket-status-config-row.tsx +65 -34
- package/src/components/ui/ticket-status-tag.tsx +35 -22
- package/src/components/ui/timezone-select.tsx +6 -2
- package/src/components/ui/toaster.tsx +19 -17
- package/src/components/ui/truncate-text.tsx +12 -0
- package/src/components/user-display.tsx +4 -4
- package/src/components/vendor-display-button.tsx +12 -17
- package/src/components/vendor-icon.tsx +25 -7
- package/src/components/vendor-tag.tsx +5 -1
- package/src/loading-standard/engine.mjs +3124 -0
- package/src/loading-standard/rule-examples.mjs +580 -0
- package/src/styles/index.css +1 -0
- package/src/styles/product-screen-frame.css +33 -0
- package/src/types/logs.types.ts +3 -3
- package/src/types/permissions.ts +10 -0
- package/dist/case-study-card-FMH622O5.cjs +0 -25
- package/dist/chunk-65VVJSQZ.cjs +0 -109
- package/dist/chunk-C42WIUH6.cjs +0 -42
- package/dist/chunk-LGNJTSKB.cjs +0 -699
- package/dist/components/chat/chat-header-icon-button.d.ts +0 -18
- package/dist/components/chat/chat-header-icon-button.d.ts.map +0 -1
- package/dist/components/chat/chat-header-icon-button.js +0 -27
- package/dist/components/chat/chat-sidebar-skeleton.d.ts +0 -12
- package/dist/components/chat/chat-sidebar-skeleton.d.ts.map +0 -1
- package/dist/components/chat/chat-sidebar-skeleton.js +0 -63
- package/dist/components/chat/mingo-onboarding-card-skeleton.d.ts +0 -45
- package/dist/components/chat/mingo-onboarding-card-skeleton.d.ts.map +0 -1
- package/dist/components/chat/mingo-onboarding-card-skeleton.js +0 -70
- package/dist/components/chat/msp-organization-card-skeleton.d.ts +0 -12
- package/dist/components/chat/msp-organization-card-skeleton.d.ts.map +0 -1
- package/dist/components/chat/msp-organization-card-skeleton.js +0 -26
- package/dist/components/features/board/ticket-card-skeleton.d.ts +0 -5
- package/dist/components/features/board/ticket-card-skeleton.d.ts.map +0 -1
- package/dist/components/features/board/ticket-card-skeleton.js +0 -41
- package/dist/components/features/devices/device-selector/device-selector-skeleton.d.ts +0 -28
- package/dist/components/features/devices/device-selector/device-selector-skeleton.d.ts.map +0 -1
- package/dist/components/features/devices/device-selector/device-selector-skeleton.js +0 -10
- package/dist/components/loading/card-skeleton.d.ts +0 -41
- package/dist/components/loading/card-skeleton.d.ts.map +0 -1
- package/dist/components/loading/card-skeleton.js +0 -141
- package/dist/components/loading/category-card-skeleton.d.ts +0 -6
- package/dist/components/loading/category-card-skeleton.d.ts.map +0 -1
- package/dist/components/loading/category-card-skeleton.js +0 -27
- package/dist/components/loading/category-vendor-selector-skeleton.d.ts +0 -13
- package/dist/components/loading/category-vendor-selector-skeleton.d.ts.map +0 -1
- package/dist/components/loading/category-vendor-selector-skeleton.js +0 -32
- package/dist/components/loading/deprecated-layout-skeletons.d.ts +0 -25
- package/dist/components/loading/deprecated-layout-skeletons.d.ts.map +0 -1
- package/dist/components/loading/deprecated-layout-skeletons.js +0 -78
- package/dist/components/loading/device-card-skeleton.d.ts +0 -40
- package/dist/components/loading/device-card-skeleton.d.ts.map +0 -1
- package/dist/components/loading/device-card-skeleton.js +0 -54
- package/dist/components/loading/margin-report-skeleton.d.ts +0 -14
- package/dist/components/loading/margin-report-skeleton.d.ts.map +0 -1
- package/dist/components/loading/margin-report-skeleton.js +0 -99
- package/dist/components/loading/msp-profile-form-skeleton.d.ts +0 -28
- package/dist/components/loading/msp-profile-form-skeleton.d.ts.map +0 -1
- package/dist/components/loading/msp-profile-form-skeleton.js +0 -28
- package/dist/components/loading/organization-card-skeleton.d.ts +0 -51
- package/dist/components/loading/organization-card-skeleton.d.ts.map +0 -1
- package/dist/components/loading/organization-card-skeleton.js +0 -88
- package/dist/components/loading/organization-icon-skeleton.d.ts +0 -49
- package/dist/components/loading/organization-icon-skeleton.d.ts.map +0 -1
- package/dist/components/loading/organization-icon-skeleton.js +0 -35
- package/dist/components/loading/page-layout-skeleton.d.ts +0 -45
- package/dist/components/loading/page-layout-skeleton.d.ts.map +0 -1
- package/dist/components/loading/page-layout-skeleton.js +0 -124
- package/dist/components/loading/settings-form-skeleton.d.ts +0 -19
- package/dist/components/loading/settings-form-skeleton.d.ts.map +0 -1
- package/dist/components/loading/settings-form-skeleton.js +0 -42
- package/dist/components/loading/wizard-layout-skeleton.d.ts +0 -9
- package/dist/components/loading/wizard-layout-skeleton.d.ts.map +0 -1
- package/dist/components/loading/wizard-layout-skeleton.js +0 -21
- package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts +0 -21
- package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.d.ts.map +0 -1
- package/dist/components/onboarding-guides/onboarding-guides-catalog-skeleton.js +0 -35
- package/dist/components/profile/ProfileLoadingSkeleton.d.ts +0 -8
- package/dist/components/profile/ProfileLoadingSkeleton.d.ts.map +0 -1
- package/dist/components/profile/ProfileLoadingSkeleton.js +0 -24
- package/dist/components/shared/detail-page-skeleton.d.ts +0 -18
- package/dist/components/shared/detail-page-skeleton.d.ts.map +0 -1
- package/dist/components/shared/detail-page-skeleton.js +0 -68
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts +0 -30
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.d.ts.map +0 -1
- package/dist/components/shared/roadmap/roadmap-grid-skeleton.js +0 -21
- package/dist/components/ui/data-table/data-table-skeleton.d.ts +0 -74
- package/dist/components/ui/data-table/data-table-skeleton.d.ts.map +0 -1
- package/dist/components/ui/data-table/data-table-skeleton.js +0 -150
- package/dist/components/ui/file-manager/file-manager-skeleton.d.ts +0 -10
- package/dist/components/ui/file-manager/file-manager-skeleton.d.ts.map +0 -1
- package/dist/components/ui/file-manager/file-manager-skeleton.js +0 -55
- package/dist/components/ui/table/index.d.ts +0 -17
- package/dist/components/ui/table/index.d.ts.map +0 -1
- package/dist/components/ui/table/index.js +0 -25
- package/dist/components/ui/table/table-cell.d.ts +0 -4
- package/dist/components/ui/table/table-cell.d.ts.map +0 -1
- package/dist/components/ui/table/table-cell.js +0 -19
- package/dist/components/ui/table/table-column-filter-dropdown.d.ts +0 -21
- package/dist/components/ui/table/table-column-filter-dropdown.d.ts.map +0 -1
- package/dist/components/ui/table/table-column-filter-dropdown.js +0 -63
- package/dist/components/ui/table/table-description-cell.d.ts +0 -21
- package/dist/components/ui/table/table-description-cell.d.ts.map +0 -1
- package/dist/components/ui/table/table-description-cell.js +0 -24
- package/dist/components/ui/table/table-empty-state.d.ts +0 -4
- package/dist/components/ui/table/table-empty-state.d.ts.map +0 -1
- package/dist/components/ui/table/table-empty-state.js +0 -22
- package/dist/components/ui/table/table-header.d.ts +0 -4
- package/dist/components/ui/table/table-header.d.ts.map +0 -1
- package/dist/components/ui/table/table-header.js +0 -158
- package/dist/components/ui/table/table-row.d.ts +0 -5
- package/dist/components/ui/table/table-row.d.ts.map +0 -1
- package/dist/components/ui/table/table-row.js +0 -112
- package/dist/components/ui/table/table-skeleton.d.ts +0 -22
- package/dist/components/ui/table/table-skeleton.d.ts.map +0 -1
- package/dist/components/ui/table/table-skeleton.js +0 -64
- package/dist/components/ui/table/table-timestamp-cell.d.ts +0 -28
- package/dist/components/ui/table/table-timestamp-cell.d.ts.map +0 -1
- package/dist/components/ui/table/table-timestamp-cell.js +0 -36
- package/dist/components/ui/table/table.d.ts +0 -4
- package/dist/components/ui/table/table.d.ts.map +0 -1
- package/dist/components/ui/table/table.js +0 -326
- package/dist/components/ui/table/types.d.ts +0 -289
- package/dist/components/ui/table/types.d.ts.map +0 -1
- package/dist/components/ui/table/types.js +0 -1
- package/dist/components/ui/table/use-table-motion.d.ts +0 -24
- package/dist/components/ui/table/use-table-motion.d.ts.map +0 -1
- package/dist/components/ui/table/use-table-motion.js +0 -20
- package/dist/components/ui/table/utils.d.ts +0 -10
- package/dist/components/ui/table/utils.d.ts.map +0 -1
- package/dist/components/ui/table/utils.js +0 -34
- package/src/components/chat/chat-header-icon-button.tsx +0 -41
- package/src/components/chat/chat-sidebar-skeleton.tsx +0 -89
- package/src/components/chat/mingo-onboarding-card-skeleton.tsx +0 -146
- package/src/components/chat/msp-organization-card-skeleton.tsx +0 -33
- package/src/components/features/board/ticket-card-skeleton.tsx +0 -41
- package/src/components/features/devices/device-selector/device-selector-skeleton.tsx +0 -38
- package/src/components/loading/card-skeleton.tsx +0 -259
- package/src/components/loading/category-card-skeleton.tsx +0 -36
- package/src/components/loading/category-vendor-selector-skeleton.tsx +0 -51
- package/src/components/loading/deprecated-layout-skeletons.tsx +0 -133
- package/src/components/loading/device-card-skeleton.tsx +0 -100
- package/src/components/loading/margin-report-skeleton.tsx +0 -153
- package/src/components/loading/msp-profile-form-skeleton.tsx +0 -59
- package/src/components/loading/organization-card-skeleton.tsx +0 -130
- package/src/components/loading/organization-icon-skeleton.tsx +0 -95
- package/src/components/loading/page-layout-skeleton.tsx +0 -192
- package/src/components/loading/settings-form-skeleton.tsx +0 -55
- package/src/components/loading/wizard-layout-skeleton.tsx +0 -35
- package/src/components/onboarding-guides/onboarding-guides-catalog-skeleton.tsx +0 -48
- package/src/components/profile/ProfileLoadingSkeleton.tsx +0 -31
- package/src/components/shared/detail-page-skeleton.tsx +0 -105
- package/src/components/shared/roadmap/roadmap-grid-skeleton.tsx +0 -54
- package/src/components/ui/data-table/data-table-skeleton.tsx +0 -229
- package/src/components/ui/file-manager/file-manager-skeleton.tsx +0 -110
- package/src/components/ui/table/index.ts +0 -34
- package/src/components/ui/table/table-cell.tsx +0 -28
- package/src/components/ui/table/table-column-filter-dropdown.tsx +0 -76
- package/src/components/ui/table/table-description-cell.tsx +0 -46
- package/src/components/ui/table/table-empty-state.tsx +0 -24
- package/src/components/ui/table/table-header.tsx +0 -182
- package/src/components/ui/table/table-row.tsx +0 -133
- package/src/components/ui/table/table-skeleton.tsx +0 -102
- package/src/components/ui/table/table-timestamp-cell.tsx +0 -81
- package/src/components/ui/table/table.tsx +0 -410
- package/src/components/ui/table/types.ts +0 -335
- package/src/components/ui/table/use-table-motion.ts +0 -48
- package/src/components/ui/table/utils.ts +0 -55
|
@@ -0,0 +1,3124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE loading-standard rule engine: one AST reading (the TypeScript compiler API) of the loading standard, shipped in
|
|
3
|
+
* the lib (`src/loading-standard/engine.mjs`, plain ESM so a host's script imports it by file path from its installed
|
|
4
|
+
* copy) and used by the lib's own test (`src/__tests__/loading-standard.test.ts`) and by every host's gate
|
|
5
|
+
* (openframe-frontend's `scripts/lib/loading-gate.mjs`, run as `check-page-skeletons` / `check-skeleton-full-page`).
|
|
6
|
+
* THIS HEADER IS THE ONE STATEMENT OF THE RULES: a host's gate points here and restates none of them.
|
|
7
|
+
*
|
|
8
|
+
* Every rule reads COMPONENTS (a function body), never lines: imports, `as` renames, local aliases
|
|
9
|
+
* (`const X = YSkeleton`), namespace imports and `createElement(Y)` resolve to the name they stand for, a condition is
|
|
10
|
+
* read as an expression however many lines it spans, and a value read through a local (`const busy = q.isLoading`,
|
|
11
|
+
* `const user = query.data`, `const HIDE = 'max-md:hidden'`) is read as what it holds. A finding names its rule, its
|
|
12
|
+
* file and its line.
|
|
13
|
+
*
|
|
14
|
+
* READ DATA is a value that is missing until a read answers: `data`, `xData`, `x.data` (and a field of them), anything
|
|
15
|
+
* a data hook returns (a `use*` hook that calls a query: `useQuery`, `useInfiniteQuery`, `useSWR`, Relay's readers,
|
|
16
|
+
* or a hook that calls one; a hook the indexed source does not declare counts as one), destructured or not, and a
|
|
17
|
+
* local aliasing any of these. A READ FLAG is a loading flag a hook returns (`isLoading`, `isPending`, `loading`, a
|
|
18
|
+
* `status === 'loading'`), never a context's (`use*Context`, which folds the scope itself).
|
|
19
|
+
*
|
|
20
|
+
* THE RULES (ids as reported; `RULES` below holds the one-line reason of each):
|
|
21
|
+
*
|
|
22
|
+
* ONE_SWITCH `<YSkeleton>` (or an alias of it, or `createElement(YSkeleton)`, or `{YSkeleton}` handed to a
|
|
23
|
+
* prop) rendered outside Y's module (the file declaring Y or YSkeleton), when Y switches itself
|
|
24
|
+
* (its function calls the loading scope); and a skeleton primitive drawn in the loading branch
|
|
25
|
+
* of `loading ? <Bars /> : <Y />` for such a Y. Render `<Y />`: it draws its own skeleton. A
|
|
26
|
+
* `*Skeleton` function may compose a child's skeleton, unless its module renders `<Y />` elsewhere
|
|
27
|
+
* (then the skeleton renders `<Y />` too: one structure).
|
|
28
|
+
* NO_SCOPE_SWITCH The same where Y never switches itself (a component and its skeleton in one folder included),
|
|
29
|
+
* a `*Skeleton` with no component of its own (a skeleton primitive excepted), and a loading
|
|
30
|
+
* component not named `*Skeleton` (`<XLoading />`, `<XPlaceholder />`) rendered outside its
|
|
31
|
+
* module: Y should draw its own skeleton on the scope, and the parent render `<Y />`.
|
|
32
|
+
* PROP_ONLY_SWITCH A component that returns a skeleton on a boolean of its own (a prop, `props.x`, a local made
|
|
33
|
+
* from them) or on a read flag of its own (`const { isLoading } = useQuery(…)`) and never calls
|
|
34
|
+
* the loading scope in THAT component: inside a scope it draws the loaded form with no data. Its
|
|
35
|
+
* one switch is `useLoadingScope() || loading`.
|
|
36
|
+
* NOTHING_WHILE_LOADING Nothing, or a blank box, drawn while loading: `if (loading) return null | false | <div />`,
|
|
37
|
+
* `loading ? null : <X />`, `!loading ? <X /> : null`, `{!loading && <X />}` (in a component that
|
|
38
|
+
* does not switch), `{loading || <X />}`, the same keyed on read data (`if (!tickets) return
|
|
39
|
+
* null`, `data ? <X /> : null`), and a `next/dynamic` import with no `loading:` fallback. Exempt
|
|
40
|
+
* once the read has answered in that component (a suspending read, a loading branch or the scope
|
|
41
|
+
* above it), inside a list's per-item callback, a scope `return null` whose loaded return is
|
|
42
|
+
* marked `data-loading-optional` or is a Relay reader (`<XQuery />`), a part that takes no room
|
|
43
|
+
* (a modal, a toast), and a flag that is an ACTION's progress (handed to a `*Button`'s `loading`
|
|
44
|
+
* or `disabled` in the same component). A local holding the scope is a loading flag.
|
|
45
|
+
* SPINNER `animate-spin` (or `animate-[spin…`) anywhere but on an action's progress icon (`Loader2`,
|
|
46
|
+
* `RefreshCw`), and a progress icon keyed on a READ flag (through a local too) or returned as a
|
|
47
|
+
* component's whole output.
|
|
48
|
+
* LOADER_COMPONENT A loader as a loading state (`PageLoader`, `CompactPageLoader`, `ContentLoader`, `CardLoader`,
|
|
49
|
+
* `DetailLoader`, `FormLoader`, `ListLoader`, `ChatCardLoader`, `OpenmspHeartbeatLoader`,
|
|
50
|
+
* `Spinner`).
|
|
51
|
+
* LOADING_TEXT Waiting copy as a loading state: a text node starting "Loading" / "Checking", a "Loading…",
|
|
52
|
+
* "loading…", "Fetching…", "Please wait…" (any case, `...` or `…`) string or text node, a
|
|
53
|
+
* "Loading" placeholder. Labels, `aria-label`, `title` and a status announcement are not text.
|
|
54
|
+
* OWN_PULSE A pulse or skeleton colour of one's own (`animate-pulse`, `animate-[pulse…`, `bg-ods-skeleton`,
|
|
55
|
+
* `animation-duration`, an inline `animation: 'pulse …'`): one pulse, the lib's.
|
|
56
|
+
* SUSPENSE_FALLBACK A `Suspense` / `React.Suspense` with no fallback, or a `Suspense` / `LoadingSuspense` fallback
|
|
57
|
+
* of `null`, `undefined`, `false`, `''`, an empty fragment or a blank element (`<div />`), and a
|
|
58
|
+
* `LoadingFallback` drawing nothing.
|
|
59
|
+
* HAND_PAIR A hand-built `<Suspense fallback={<LoadingFallback>…}>` (use `LoadingSuspense`), or a hand
|
|
60
|
+
* `<LoadingRegion>` around a `<LoadingScope>` / `<LoadingWhen>` (use `LoadingFallback`).
|
|
61
|
+
* WHOLE_VIEW_SKELETON A component declared or rendered as `*PageSkeleton`, `*ScreenSkeleton`, `*DetailSkeleton`,
|
|
62
|
+
* `*ArticleSkeleton`, `*ViewSkeleton`, `*HomeSkeleton`, `*DeckSkeleton`, `*EditorSkeleton`,
|
|
63
|
+
* `*CenterSkeleton`, `*CatalogSkeleton`, `*FormSkeleton`, `*LayoutSkeleton`,
|
|
64
|
+
* `*DashboardSkeleton`, `*LoadingLayout` or `*(Page|Screen|View|Panel|Dashboard)Loading` (a
|
|
65
|
+
* declaration marked `@deprecated` is a kept alias: not counted, its renders are
|
|
66
|
+
* DEPRECATED_SKELETON's).
|
|
67
|
+
* FOLD_PROP A `fold` prop on any element (a skeleton draws its full count: `skeletonCount`).
|
|
68
|
+
* DEPRECATED_SKELETON A render of a skeleton the lib marks `@deprecated` (read from the lib's source).
|
|
69
|
+
* RETIRED_API The retired first-paint memory, stand-in data (`standIn`, `Hold`, any `Hold*` name such as
|
|
70
|
+
* `HoldText`; the primitives `TextHold` / `LayoutHold` are not) and `StreamedPage`'s
|
|
71
|
+
* `skeleton` / `memory` / `remember` props.
|
|
72
|
+
* TYPED_COUNT A count typed at the place that draws it: a literal (after resolving a constant to its
|
|
73
|
+
* initializer, both arms of a ternary included: `isMobile ? 3 : 10`) on a skeleton count prop
|
|
74
|
+
* (`skeletonRows`, `skeletonCount`, `loadingRows`, any `(skeleton|loading|placeholder)…(Rows|
|
|
75
|
+
* Count|Lines|Items)`, `count` on `LoadingItems`, `rows` / `count` / `lines` on a `*Skeleton`
|
|
76
|
+
* element) or on such a key of an object (`{ loadingRows: { primary: 7 } }`), the length of a
|
|
77
|
+
* list drawn while loading (`Array.from({ length })`, `Array(n)`, `[...Array(n)]`,
|
|
78
|
+
* `[0, 1, 2].map`, `[1, 2, 3, 4].map`), a field of a local object holding a literal
|
|
79
|
+
* (`const settings = { rows: 4 }` -> `settings.rows`), a literal default (other than 0) of ANY
|
|
80
|
+
* parameter or prop whose value reaches a skeleton count in its function (`skeletonRows = 8`,
|
|
81
|
+
* `{ size = 6 }` drawn as `<LoadingItems count={size}>`), a `?? 10` fallback in a count, and a
|
|
82
|
+
* module constant holding a literal-length list (`Array.from({ length: 4 })`, `[1, 2, 3, 4]`)
|
|
83
|
+
* used while loading. Text draws a typical LINE
|
|
84
|
+
* count (`*Lines`, `*Paragraphs`) only when each item is a skeleton line itself; a list of rows
|
|
85
|
+
* named `*LINES` is a list.
|
|
86
|
+
* COUNT_SOURCE A skeleton count that names neither a page size, a limit, `first`, nor a declared
|
|
87
|
+
* `*_SKELETON_*` constant: a count computed by a call (`rowsFor(w)`), read from a response
|
|
88
|
+
* (`data?.total ?? 0`), or added to a list's length (`rows.length + 2`). Not counted: a count the
|
|
89
|
+
* caller passes (a prop, or a field of one; checked where its caller names it), a skeleton-count
|
|
90
|
+
* field of a value (`scroll.skeletonRows`, checked where its key is set), a named count scaled
|
|
91
|
+
* or split (`PAGE_SIZE * 2`, `rows - 1`), the rest of a page (`pageSize - items.length`), the
|
|
92
|
+
* length of a list, and a value that is no number (`rows={[{ width }]}`). Also a `*_SKELETON_*`
|
|
93
|
+
* count in a component whose list has a page size (`PAGE_SIZE`, `pageSize`, `perPage`): a paged
|
|
94
|
+
* list draws its page.
|
|
95
|
+
* COUNT_CAP A count capped: `Math.min(pageSize, FOLD | CONSTANT | 8 | a local holding a number)`, a ternary
|
|
96
|
+
* or comparison cap (`PAGE_SIZE > 6 ? 6 : PAGE_SIZE`), or a count named `*FOLD*`, `*VISIBLE*` or
|
|
97
|
+
* `*SCREEN*` (`Math.min(PAGE_SIZE, rows.length)`, the last page, and a clamp are fine).
|
|
98
|
+
* HIDDEN_ITEMS A loading item hidden per breakpoint (`hidden md:block`, `max-md:hidden`, `md:hidden` on the
|
|
99
|
+
* item's own element, through a class constant or a conditional class too), a container hiding
|
|
100
|
+
* items past the Nth (`[&>*:nth-child(n+N)]:hidden`), and an item callback returning nothing past
|
|
101
|
+
* the Nth (`i < 4 ? <Row /> : null`) while loading.
|
|
102
|
+
* ITEM_MARK A loading list (`LoadingItems`, a list whose count is a page size, limit or `*_SKELETON_*`
|
|
103
|
+
* constant, or a pass-through count prop drawn while loading) whose items are not marked
|
|
104
|
+
* `data-loading-item` (or `loadingItemProps(…)`, `listItem`, `listItems` on the item element, or
|
|
105
|
+
* on the element the item's component renders, read from its declaration through its import),
|
|
106
|
+
* and the loaded list beside it: the other branch of the same condition, or the list the
|
|
107
|
+
* component returns after `if (loading) return …`. The cells of one row (a count named `columns`,
|
|
108
|
+
* `cols`, `cells`) are not list items.
|
|
109
|
+
* OPTIONAL_MARK In a component that switches on the scope, a part left out while loading (`{!loading && <X />}`)
|
|
110
|
+
* or keyed on a read field (`{record.note && <X />}`, `{record?.x ? <X /> : null}`) that does not
|
|
111
|
+
* carry `data-loading-optional`. Not counted: an error / offline state, the loaded half of an
|
|
112
|
+
* explicit pair (a `{loading && …}` sibling before it), a flag-like field (`show…`, `is…`,
|
|
113
|
+
* `has…`) or a configuration / presentation field (`actions.copy`, `anchorProps.href`, `icon`), an
|
|
114
|
+
* empty-state notice (`noX`, `!hasX`, `length === 0`), a part inside a list's per-item callback,
|
|
115
|
+
* a link of a ternary chain, a part with no room in the page (a modal, a popover's content, an
|
|
116
|
+
* `sr-only` / `role="status"` announcement), and a part where the loading form draws its OWN
|
|
117
|
+
* version of the region (after an early `if (loading) return …`, in the loaded half of
|
|
118
|
+
* `loading ? <A /> : <B />`, or the `else` of an `if` on the loading state). A component that
|
|
119
|
+
* reads the scope but draws no loading form is not switching.
|
|
120
|
+
* PAGE_CODE A host's page code (`config.pageCode` says which files) draws no skeleton: a skeleton primitive,
|
|
121
|
+
* a `*Skeleton` declared or rendered (`components: false` allows rendering a component's exported
|
|
122
|
+
* skeleton), a route-level `loading.tsx`.
|
|
123
|
+
* OPT_OUT An opt-out that is not the rule-scoped form, names a rule this gate does not run, gives no
|
|
124
|
+
* reason, or suppresses nothing.
|
|
125
|
+
*
|
|
126
|
+
* ALLOWED FORMS (no rule fires): a `next/dynamic` `loading: () => <YSkeleton />` fallback (inside the `dynamic(` call);
|
|
127
|
+
* a registry entry's own `skeleton:` slot drawing the card its `render:` draws (`XChatCard` is X's card); an action's
|
|
128
|
+
* progress icon; `return null` after a suspending read or a loading branch; error states; a ternary choosing between
|
|
129
|
+
* two forms of one slot; pass-through counts; a safety-capped list drawing a declared `*_SKELETON_*` constant.
|
|
130
|
+
*
|
|
131
|
+
* OPT-OUT: ONE form, rule-scoped and single-line: `// loading-standard-ok(RULE_ID): <why>` (or the same inside
|
|
132
|
+
* `/* … *\/`), at the end of the finding's line or alone on the line directly above it. It lifts that one rule on
|
|
133
|
+
* that one line, and an opt-out that lifts nothing is itself a finding (OPT_OUT).
|
|
134
|
+
*
|
|
135
|
+
* CONFIG: `CONFIGS.lib` and `CONFIGS.frontend` hold each repository's rule set, reported trees, page code and
|
|
136
|
+
* exemptions, declared once here.
|
|
137
|
+
*/
|
|
138
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
139
|
+
import { createRequire } from 'node:module';
|
|
140
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
141
|
+
|
|
142
|
+
// The compiler is the HOST's (a dev dependency of every repository that runs the gate), resolved from where this
|
|
143
|
+
// module is installed: the lib does not ship TypeScript to its consumers.
|
|
144
|
+
const ts = createRequire(import.meta.url)('typescript');
|
|
145
|
+
|
|
146
|
+
/** Every rule id, with the reason a finding of it prints. */
|
|
147
|
+
export const RULES = {
|
|
148
|
+
ONE_SWITCH: 'a parent picks the skeleton of a component that switches itself (render the component)',
|
|
149
|
+
NO_SCOPE_SWITCH:
|
|
150
|
+
'a parent picks the skeleton of a component that never switches itself (the component should switch on useLoadingScope())',
|
|
151
|
+
PROP_ONLY_SWITCH: 'a switch on the loading prop alone (switch on useLoadingScope() || loading)',
|
|
152
|
+
NOTHING_WHILE_LOADING: 'nothing drawn while loading (render the component in a loading scope)',
|
|
153
|
+
SPINNER: 'a spinner as a loading state',
|
|
154
|
+
LOADER_COMPONENT: 'a loader component as a loading state (render the components in a loading scope)',
|
|
155
|
+
LOADING_TEXT: '"Loading" text as a loading state (draw the component skeleton)',
|
|
156
|
+
OWN_PULSE: 'a pulse or skeleton colour of its own (use the lib skeletons)',
|
|
157
|
+
SUSPENSE_FALLBACK: 'a Suspense with no fallback (use LoadingSuspense / LoadingFallback)',
|
|
158
|
+
HAND_PAIR: 'a hand-built loading pair (use the lib wrapper)',
|
|
159
|
+
WHOLE_VIEW_SKELETON: 'a whole-view skeleton (the view itself in a loading scope is its loading state)',
|
|
160
|
+
FOLD_PROP: 'a `fold` prop (a skeleton draws its full count: skeletonCount)',
|
|
161
|
+
DEPRECATED_SKELETON: 'deprecated in the lib (render the component in a loading scope)',
|
|
162
|
+
RETIRED_API: 'a retired loading API',
|
|
163
|
+
TYPED_COUNT: "a typed skeleton count (name the list's page size, limit or a *_SKELETON_* constant)",
|
|
164
|
+
COUNT_SOURCE: 'a skeleton count that does not name a page size, a limit or a *_SKELETON_* constant',
|
|
165
|
+
COUNT_CAP: "a page size or limit capped (a list's skeleton draws its full page)",
|
|
166
|
+
HIDDEN_ITEMS: 'loading items hidden per breakpoint (a skeleton draws its full count at every width)',
|
|
167
|
+
ITEM_MARK: 'a loading list with no data-loading-item (or loadingItemProps) on its items',
|
|
168
|
+
OPTIONAL_MARK: 'a part left out while loading has no data-loading-optional',
|
|
169
|
+
PAGE_CODE: 'page code draws no skeleton (render the components: each draws its own)',
|
|
170
|
+
OPT_OUT: 'an opt-out that is not `// loading-standard-ok(RULE_ID): <why>` or that lifts nothing',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const ALL_RULES = Object.keys(RULES);
|
|
174
|
+
|
|
175
|
+
/** The files a host's PAGE CODE is (its route files, every file of a route folder, its screens, its `lib/`). */
|
|
176
|
+
const ROUTE_FILE = /(?:^|\/)(?:page|layout|loading|template|not-found|error)\.tsx$/;
|
|
177
|
+
const SCREEN_FILE = /-page(?:-ssr|-client)?\.tsx$/;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Each repository's rule set and exemptions. `trees` are the folders reported (relative to the tree's root; all when
|
|
181
|
+
* absent). `pageCode(rel, isFile)` says whether a file is page code (`{ components }`: whether it may render a
|
|
182
|
+
* component's exported skeleton) or not (null); `isFile(rel)` asks whether a file exists in the tree. An exemption
|
|
183
|
+
* names the files (a RegExp over the path relative to the tree's root) and the rules it lifts, and says why.
|
|
184
|
+
*/
|
|
185
|
+
export const CONFIGS = {
|
|
186
|
+
frontend: {
|
|
187
|
+
rules: ALL_RULES,
|
|
188
|
+
trees: ['app', 'components', 'lib', 'hooks'],
|
|
189
|
+
/**
|
|
190
|
+
* Every route file under `app/`, every other `.tsx` in a ROUTE FOLDER (a folder under `app/` holding a `page.tsx`
|
|
191
|
+
* or a `layout.tsx`), every screen file (`*-page.tsx`, `*-page-client.tsx`, `*-page-ssr.tsx`) and every `.tsx`
|
|
192
|
+
* under `lib/` (which may render a component's exported skeleton).
|
|
193
|
+
*/
|
|
194
|
+
pageCode(rel, isFile) {
|
|
195
|
+
if (!rel.endsWith('.tsx')) return null;
|
|
196
|
+
if (rel.startsWith('lib/')) return { components: false };
|
|
197
|
+
if (SCREEN_FILE.test(rel) && (rel.startsWith('app/') || rel.startsWith('components/')))
|
|
198
|
+
return { components: true };
|
|
199
|
+
if (!rel.startsWith('app/')) return null;
|
|
200
|
+
const folder = dirname(rel);
|
|
201
|
+
if (ROUTE_FILE.test(rel) || isFile(join(folder, 'page.tsx')) || isFile(join(folder, 'layout.tsx')))
|
|
202
|
+
return { components: true };
|
|
203
|
+
return null;
|
|
204
|
+
},
|
|
205
|
+
exempt: [],
|
|
206
|
+
},
|
|
207
|
+
lib: {
|
|
208
|
+
rules: ALL_RULES,
|
|
209
|
+
exempt: [
|
|
210
|
+
{
|
|
211
|
+
why: "the lib's own loading primitives: the one pulse, the scope, the fallback and the Suspense wrapper are built here",
|
|
212
|
+
files:
|
|
213
|
+
/(?:^|\/)components\/(?:loading\/(?:loading-fallback|loading-region|unified-skeleton)|ui\/(?:loading-scope|skeleton[\w-]*|text-line-skeleton))\.tsx?$/,
|
|
214
|
+
rules: ['OWN_PULSE', 'HAND_PAIR', 'SUSPENSE_FALLBACK', 'NOTHING_WHILE_LOADING'],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
why: "the loaders themselves (declared, never a screen state in the lib), the Button's own action progress, and the chat registry's card loader (a card read by id, drawn by the card's own skeleton)",
|
|
218
|
+
files:
|
|
219
|
+
/(?:^|\/)components\/(?:ui\/(?:[\w-]+-)?loader|loading\/openmsp-heartbeat|ui\/button\/button|chat\/entity-cards\/dispatch)\.tsx$/,
|
|
220
|
+
rules: ['LOADER_COMPONENT', 'SPINNER', 'OWN_PULSE'],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
227
|
+
// Vocabulary
|
|
228
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
const SCOPE_HOOK = 'useLoadingScope';
|
|
231
|
+
const LOADING_FLAG = /^(?:loading|isLoading|isFetching|isPending|pending|isRefetching|isInitialLoading|isLoadingMore)$/;
|
|
232
|
+
const READ_FLAG = /^(?:isLoading|isFetching|isRefetching|isInitialLoading|isPending|loading)$/;
|
|
233
|
+
const READ_DATA = /^(?:data|[a-z]\w*Data)$/;
|
|
234
|
+
const SWITCH_FLAG = /loading|pending|skeleton|busy|fetching|waiting/i;
|
|
235
|
+
const LOCAL_HOOK = /(?:^|\.)(?:useMemo|useState|useCallback|useRef|useReducer|useDeferredValue|useId|useTransition)$/;
|
|
236
|
+
const SUSPENDING_READ =
|
|
237
|
+
/^(?:useLazyLoadQuery|usePreloadedQuery|useFragment|usePaginationFragment|useRefetchableFragment|useSuspenseQuery|useSuspenseQueries|use)$/;
|
|
238
|
+
/** The read primitives a DATA HOOK calls (directly, or through another data hook). */
|
|
239
|
+
const QUERY_HOOK =
|
|
240
|
+
/^(?:useQuery|useQueries|useInfiniteQuery|useSuspenseQuery|useSuspenseQueries|useSuspenseInfiniteQuery|useSWR|useSWRInfinite|useLazyLoadQuery|usePreloadedQuery|useFragment|usePaginationFragment|useRefetchableFragment|useQueryLoader)$/;
|
|
241
|
+
/** Hooks that never return read data (routing, forms, layout, stores, contexts). */
|
|
242
|
+
const NOT_DATA_HOOK =
|
|
243
|
+
/^(?:useRouter|usePathname|useSearchParams|useParams|useSelectedLayoutSegments?|useTranslation|useTheme|useMediaQuery|useIsMobile|useBreakpoint|useToast|useForm|useFormContext|useFormState|useWatch|useController|useFieldArray|useSelector|useDispatch|useMutation|useQueryClient|useRelayEnvironment|useLoadingScope|use\w*(?:Context|Store|State|Mutation|Ref|Callback|Effect|Handler|Toggle|Modal|Dialog)|use\w*(?:Flag|Gate|Feature)s?)$/;
|
|
244
|
+
/** Fields of a hook's result that are not its data (flags, errors, actions). */
|
|
245
|
+
const NOT_DATA_FIELD =
|
|
246
|
+
/^(?:error|errors|status|refetch|fetchNextPage|fetchMore|loadMore|reset|mutate|mutateAsync|hasNextPage|hasMore|set[A-Z]\w*|on[A-Z]\w*|is[A-Z]\w*|has[A-Z]\w*)$/;
|
|
247
|
+
const PROGRESS_ICONS = new Set(['Loader2', 'RefreshCw']);
|
|
248
|
+
const LOADERS = new Set([
|
|
249
|
+
'PageLoader',
|
|
250
|
+
'CompactPageLoader',
|
|
251
|
+
'ContentLoader',
|
|
252
|
+
'CardLoader',
|
|
253
|
+
'DetailLoader',
|
|
254
|
+
'FormLoader',
|
|
255
|
+
'ListLoader',
|
|
256
|
+
'ChatCardLoader',
|
|
257
|
+
'OpenmspHeartbeatLoader',
|
|
258
|
+
'Spinner',
|
|
259
|
+
]);
|
|
260
|
+
/** The lib's skeleton parts: what a `*Skeleton` (or a component's own loading branch) is built from. */
|
|
261
|
+
const PRIMITIVE =
|
|
262
|
+
/^(?:UnifiedSkeleton|TextLineSkeleton|FieldValueSkeleton|TextHold|LayoutHold|Skeleton)$|^(?:TextSkeleton|MediaSkeleton|InteractiveSkeleton)\./;
|
|
263
|
+
/** The lib's loading wrappers (not loading states of a component). */
|
|
264
|
+
const LOADING_WRAPPER = /^(?:Loading(?:Items|When|Fallback|Suspense|Scope|Region)|InertWhileLoading)$/;
|
|
265
|
+
const WHOLE_VIEW =
|
|
266
|
+
/^[A-Z]\w*(?:(?:Page|Screen|Detail|Article|View|Home|Deck|Editor|Center|Catalog|Form|Layout|Dashboard)Skeleton|LoadingLayout|(?:Page|Screen|View|Panel|Dashboard)Loading)$/;
|
|
267
|
+
/** A loading component that is not named `*Skeleton` (`XLoading`; `XPlaceholder` when drawn while loading). */
|
|
268
|
+
const LOADING_COMPONENT = /^[A-Z]\w*Loading$/;
|
|
269
|
+
const PLACEHOLDER_COMPONENT = /^[A-Z]\w*Placeholder$/;
|
|
270
|
+
const MEMORY = new Set([
|
|
271
|
+
'pageFirstPaint',
|
|
272
|
+
'firstPaintKvStore',
|
|
273
|
+
'createFirstPaintMemory',
|
|
274
|
+
'careersSeed',
|
|
275
|
+
'refreshCareersSeed',
|
|
276
|
+
'lastKnownHomeLayout',
|
|
277
|
+
'rememberHomeLayout',
|
|
278
|
+
]);
|
|
279
|
+
const STAND_IN = new Set(['standIn', 'isStandIn', 'STAND_IN_MARK', 'useHolds', 'HELD_CONTROL_CLASS']);
|
|
280
|
+
/** The retired stand-in family (`Hold`, `HoldText`, `HoldStandIn`, …); the primitives `TextHold` / `LayoutHold` are not. */
|
|
281
|
+
const HOLD_NAME = /^Hold(?:[A-Z]\w*)?$/;
|
|
282
|
+
const isRetiredName = name => MEMORY.has(name) || STAND_IN.has(name) || HOLD_NAME.test(name);
|
|
283
|
+
const COUNT_ATTR = /^(?:skeletonRows|skeletonCount|(?:skeleton|loading|placeholder)\w*(?:Rows|Count|Lines|Items))$/i;
|
|
284
|
+
/**
|
|
285
|
+
* THE count vocabulary, declared once: where a list's count comes from (its PAGE: a page size or a limit) and what caps
|
|
286
|
+
* it (a fold, a screen, what is visible). Every count rule reads these two lists.
|
|
287
|
+
*/
|
|
288
|
+
const PAGE_SOURCE = {
|
|
289
|
+
pageSize: ['PAGE_SIZE', 'PER_PAGE', 'pageSize', 'PageSize', 'page_size', 'perPage', 'PerPage', 'per_page'],
|
|
290
|
+
/** A limit: `LIMIT` anywhere in a constant, `limit` / `Limit` ending a name. */
|
|
291
|
+
limit: ['LIMIT', 'limit$', 'Limit$'],
|
|
292
|
+
};
|
|
293
|
+
const CAP_WORDS = ['fold', 'visible', 'screen'];
|
|
294
|
+
/** A page size (not a safety limit): a list that has one draws it, never a `*_SKELETON_*` count. */
|
|
295
|
+
const PAGE_SIZE_NAME = new RegExp(PAGE_SOURCE.pageSize.join('|'));
|
|
296
|
+
/** A count that names where it comes from: a page size, a limit, `first` or a declared `*_SKELETON_*` constant. */
|
|
297
|
+
const COUNT_SOURCE_NAME = new RegExp(
|
|
298
|
+
[...PAGE_SOURCE.pageSize, ...PAGE_SOURCE.limit, '(?:^|_)SKELETON_', '^first$'].join('|'),
|
|
299
|
+
);
|
|
300
|
+
/** An argument of `Math.min` that is the page (a page size, a limit, a `*_MAX` constant), in any spelling. */
|
|
301
|
+
const PAGE_ARG = new RegExp(
|
|
302
|
+
[...PAGE_SOURCE.pageSize, ...PAGE_SOURCE.limit.map(w => w.replace('$', '\\b')), '[A-Z_]*_MAX\\b'].join('|'),
|
|
303
|
+
'i',
|
|
304
|
+
);
|
|
305
|
+
/** A count named for what it caps to (`ROW_FOLD`, `visibleRows`, `SCREEN_ROWS`). */
|
|
306
|
+
const CAP_NAME = new RegExp(CAP_WORDS.join('|'), 'i');
|
|
307
|
+
/** An argument of `Math.min` beside the page that caps it: a cap name, a constant or a number. */
|
|
308
|
+
const isCapArg = text => CAP_NAME.test(text) || /\b[A-Z][A-Z0-9_]{2,}\b|^\d+$/.test(text);
|
|
309
|
+
/** The cells of one row (`columns`, `cells`): their count never changes the row's height. */
|
|
310
|
+
const isCellCount = name => /(?:col(?:umn)?s?|cells?)$/i.test(name);
|
|
311
|
+
/** A text's line count (`lines`, `paragraphs`): a typical count of the text it stands for, not a list. */
|
|
312
|
+
const isTextLineCount = name => /lines?$|paragraphs?$/i.test(name);
|
|
313
|
+
/** A count handed in by the caller that a skeleton draws as its items. */
|
|
314
|
+
const PASSED_COUNT = /^(?:count|rows|items|rowCount|itemCount)$/;
|
|
315
|
+
/** A condition on the screen's width (`isMobile ? 3 : 10`). */
|
|
316
|
+
const WIDTH_TEST = /mobile|desktop|tablet|breakpoint|width|viewport|isSmall|isLarge|narrow/i;
|
|
317
|
+
const BREAKPOINT_HIDDEN =
|
|
318
|
+
/(?:^|\s)(?:hidden\s+(?:[\w-]+:)+(?:block|flex|grid|inline|inline-flex|inline-block|table|table-row|table-cell|contents)|(?:[\w-]+:)+hidden)(?=\s|$)|(?:^|\s)(?:[\w-]+:)+(?:block|flex|grid|inline|inline-flex|table-row|table-cell|contents)\s+hidden(?=\s|$)/;
|
|
319
|
+
const NTH_HIDDEN = /nth-child\([^)]*\)\]:hidden/;
|
|
320
|
+
const SPIN_CLASS = /\banimate-spin\b|\banimate-\[spin/;
|
|
321
|
+
const PULSE_CLASS = /\banimate-pulse\b|\banimate-\[pulse|animation-duration|\bbg-ods-skeleton\b/;
|
|
322
|
+
/** Waiting copy: "Loading"/"Checking" text nodes, and any of these words ending in an ellipsis. */
|
|
323
|
+
const WAIT_TEXT = /^(?:loading|checking)\b/i;
|
|
324
|
+
const WAIT_ELLIPSIS = /^(?:loading|checking|fetching|please wait)\b.*(?:\.\.\.|…)\s*$/i;
|
|
325
|
+
const FLAG_FIELD =
|
|
326
|
+
/^(?:is|has|show|should|can|enable|hide|with|allow)(?=[A-Z]|$)|^(?:enabled|hidden|disabled|open|visible|selected|active|expanded|collapsed|checked|loading|length)$/;
|
|
327
|
+
const CONFIG_FIELD = /^(?:icon|children|actions?|className|style|href|on[A-Z]\w*|render[A-Z]?\w*)$/;
|
|
328
|
+
/** Roots that hold configuration or presentation, never a record (`actions.copy`, `anchorProps.href`). */
|
|
329
|
+
const CONFIG_ROOT =
|
|
330
|
+
/^(?:props|this|window|document|process|config|settings|options|actions|theme|styles?|classes|t|i18n|router|navigation|form|field|meta|labels|copy|\w+Props)$/i;
|
|
331
|
+
const FAILURE = /error|offline|failed|failure/i;
|
|
332
|
+
/** The one opt-out form (see the header). */
|
|
333
|
+
const OPT_OUT_MARK = /loading-standard-ok\(\s*([A-Z_]+)\s*\):(.*)$/;
|
|
334
|
+
const OPT_OUT_ANY = /loading-standard-ok|full-page-ok/;
|
|
335
|
+
const WRAPPER_CALL = /^(?:memo|forwardRef|observer|React\.memo|React\.forwardRef)$|^with[A-Z]/;
|
|
336
|
+
/** The attributes that mark a list item for the height gate. */
|
|
337
|
+
const ITEM_MARK_ATTR = /^(?:data-loading-item|listItem|listItems)$/;
|
|
338
|
+
const OFF_SCREEN = /(?:Modal|Dialog|Drawer|Toast|Portal|Popover|Sheet|Tooltip|Status)$/;
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
341
|
+
// Files
|
|
342
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
const SKIPPED_DIRS = new Set(['node_modules', '__tests__', '__fixtures__', 'stories']);
|
|
345
|
+
const TEST_FILE = /\.(?:test|stories|spec)\.tsx?$/;
|
|
346
|
+
|
|
347
|
+
/** Every `.ts` / `.tsx` file under `dir`, without dependencies, dot folders, tests and stories. */
|
|
348
|
+
function* walkSourceFiles(dir) {
|
|
349
|
+
if (!existsSync(dir)) return;
|
|
350
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
351
|
+
if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
352
|
+
const full = join(dir, entry.name);
|
|
353
|
+
if (entry.isDirectory()) yield* walkSourceFiles(full);
|
|
354
|
+
else if (/\.tsx?$/.test(entry.name) && !/\.d\.ts$/.test(entry.name) && !TEST_FILE.test(entry.name)) yield full;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
359
|
+
// AST helpers
|
|
360
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
const isFunctionLike = node =>
|
|
363
|
+
ts.isArrowFunction(node) ||
|
|
364
|
+
ts.isFunctionExpression(node) ||
|
|
365
|
+
ts.isFunctionDeclaration(node) ||
|
|
366
|
+
ts.isMethodDeclaration(node);
|
|
367
|
+
|
|
368
|
+
function strip(node) {
|
|
369
|
+
let n = node;
|
|
370
|
+
while (
|
|
371
|
+
n &&
|
|
372
|
+
(ts.isParenthesizedExpression(n) ||
|
|
373
|
+
ts.isAsExpression(n) ||
|
|
374
|
+
ts.isNonNullExpression(n) ||
|
|
375
|
+
(ts.isSatisfiesExpression && ts.isSatisfiesExpression(n)) ||
|
|
376
|
+
ts.isTypeAssertionExpression(n))
|
|
377
|
+
)
|
|
378
|
+
n = n.expression;
|
|
379
|
+
return n;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The text of a callee / tag (`A`, `A.B`). */
|
|
383
|
+
function dottedName(node) {
|
|
384
|
+
const n = strip(node);
|
|
385
|
+
if (!n) return '';
|
|
386
|
+
if (ts.isIdentifier(n)) return n.text;
|
|
387
|
+
if (ts.isPropertyAccessExpression(n)) {
|
|
388
|
+
const left = dottedName(n.expression);
|
|
389
|
+
return left ? `${left}.${n.name.text}` : n.name.text;
|
|
390
|
+
}
|
|
391
|
+
if (n.kind === ts.SyntaxKind.ThisKeyword) return 'this';
|
|
392
|
+
return '';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The name a function-like node is known by (its declaration, the variable or property it is assigned to). */
|
|
396
|
+
function functionName(fn) {
|
|
397
|
+
if ((ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn)) && fn.name) return fn.name.text;
|
|
398
|
+
if (ts.isMethodDeclaration(fn) && ts.isIdentifier(fn.name)) return fn.name.text;
|
|
399
|
+
let p = fn.parent;
|
|
400
|
+
while (p && (ts.isParenthesizedExpression(p) || ts.isAsExpression(p))) p = p.parent;
|
|
401
|
+
while (p && ts.isCallExpression(p) && WRAPPER_CALL.test(dottedName(p.expression))) {
|
|
402
|
+
p = p.parent;
|
|
403
|
+
while (p && (ts.isParenthesizedExpression(p) || ts.isAsExpression(p))) p = p.parent;
|
|
404
|
+
}
|
|
405
|
+
if (p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) return p.name.text;
|
|
406
|
+
if (p && ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) return p.name.text;
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const isComponentName = name => /^[A-Z]/.test(name);
|
|
411
|
+
|
|
412
|
+
/** The nearest enclosing function-like node, or null. */
|
|
413
|
+
function nearestFunction(node) {
|
|
414
|
+
for (let p = node.parent; p; p = p.parent) if (isFunctionLike(p)) return p;
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The nearest enclosing function whose name is a component's (capitalised), else the outermost function, else null. */
|
|
419
|
+
function componentOf(node) {
|
|
420
|
+
let outer = null;
|
|
421
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
422
|
+
if (!isFunctionLike(p)) continue;
|
|
423
|
+
if (isComponentName(functionName(p))) return p;
|
|
424
|
+
outer = p;
|
|
425
|
+
}
|
|
426
|
+
return outer;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Every enclosing function's name, innermost first. */
|
|
430
|
+
function enclosingNames(node) {
|
|
431
|
+
const names = [];
|
|
432
|
+
for (let p = node.parent; p; p = p.parent) if (isFunctionLike(p)) names.push(functionName(p));
|
|
433
|
+
return names;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Walk `node`'s descendants, calling `visit`; `visit` returning false skips the subtree. */
|
|
437
|
+
function walk(node, visit) {
|
|
438
|
+
const go = n => {
|
|
439
|
+
if (visit(n) === false) return;
|
|
440
|
+
ts.forEachChild(n, go);
|
|
441
|
+
};
|
|
442
|
+
ts.forEachChild(node, go);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function bodyOf(fn) {
|
|
446
|
+
return fn.body ?? null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The JSX element a returned / arrow-body expression is (first JSX through parens, conditionals take the true side). */
|
|
450
|
+
function rootElement(expr) {
|
|
451
|
+
const n = strip(expr);
|
|
452
|
+
if (!n) return null;
|
|
453
|
+
if (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n)) return n;
|
|
454
|
+
if (ts.isConditionalExpression(n)) return rootElement(n.whenTrue);
|
|
455
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
456
|
+
return rootElement(n.right);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The element (opening or self-closing) carrying the attributes of a JSX element node. */
|
|
461
|
+
function openingOf(el) {
|
|
462
|
+
if (ts.isJsxElement(el)) return el.openingElement;
|
|
463
|
+
if (ts.isJsxSelfClosingElement(el)) return el;
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** The returned expression of a callback (an arrow's concise body, or its block's last `return`). */
|
|
468
|
+
function returnedExpression(fn) {
|
|
469
|
+
if (!fn) return null;
|
|
470
|
+
if (fn.body && !ts.isBlock(fn.body)) return fn.body;
|
|
471
|
+
let found = null;
|
|
472
|
+
if (fn.body)
|
|
473
|
+
walk(fn.body, n => {
|
|
474
|
+
if (isFunctionLike(n)) return false;
|
|
475
|
+
if (ts.isReturnStatement(n) && n.expression && !found) found = n.expression;
|
|
476
|
+
return undefined;
|
|
477
|
+
});
|
|
478
|
+
return found;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function attribute(opening, name) {
|
|
482
|
+
if (!opening) return null;
|
|
483
|
+
for (const attr of opening.attributes.properties) {
|
|
484
|
+
if (ts.isJsxAttribute(attr) && attr.name.getText() === name) return attr;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function attributeExpression(attr) {
|
|
490
|
+
if (!attr?.initializer) return null;
|
|
491
|
+
if (ts.isStringLiteral(attr.initializer)) return attr.initializer;
|
|
492
|
+
if (ts.isJsxExpression(attr.initializer)) return attr.initializer.expression ?? null;
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The class strings of an expression, a constant it names read as the string it holds (`MOBILE_HIDE`). */
|
|
497
|
+
function classStrings(expr, model, depth = 0) {
|
|
498
|
+
const out = stringsIn(expr);
|
|
499
|
+
const visit = n => {
|
|
500
|
+
if (ts.isIdentifier(n) && depth < 3) {
|
|
501
|
+
const decl = model.lookup(n.text, n);
|
|
502
|
+
if (decl?.kind === 'var' && decl.init && !decl.destructured)
|
|
503
|
+
out.push(...classStrings(decl.init, model, depth + 1));
|
|
504
|
+
}
|
|
505
|
+
ts.forEachChild(n, visit);
|
|
506
|
+
};
|
|
507
|
+
visit(expr);
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Every string piece (literals, template parts) inside `node`. */
|
|
512
|
+
function stringsIn(node) {
|
|
513
|
+
const out = [];
|
|
514
|
+
const take = n => {
|
|
515
|
+
if (ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) out.push(n.text);
|
|
516
|
+
else if (ts.isTemplateExpression(n)) {
|
|
517
|
+
out.push(n.head.text);
|
|
518
|
+
for (const span of n.templateSpans) out.push(span.literal.text);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
take(node);
|
|
522
|
+
walk(node, n => {
|
|
523
|
+
take(n);
|
|
524
|
+
});
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** What renders nothing: `null`, `undefined`, `false`, `''`, an empty fragment. */
|
|
529
|
+
const isNullish = node => {
|
|
530
|
+
const n = strip(node);
|
|
531
|
+
if (!n) return false;
|
|
532
|
+
if (n.kind === ts.SyntaxKind.NullKeyword || n.kind === ts.SyntaxKind.FalseKeyword) return true;
|
|
533
|
+
if (ts.isIdentifier(n) && n.text === 'undefined') return true;
|
|
534
|
+
if (ts.isJsxFragment(n) && n.children.every(c => ts.isJsxText(c) && !c.text.trim())) return true;
|
|
535
|
+
if ((ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n)) && !n.text.trim()) return true;
|
|
536
|
+
return false;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/** Nothing, or a blank box: an HTML element with no children and nothing but layout attributes (`<div className="h-64" />`). */
|
|
540
|
+
const isBlank = node => {
|
|
541
|
+
if (isNullish(node)) return true;
|
|
542
|
+
const n = strip(node);
|
|
543
|
+
if (!n || !(ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n))) return false;
|
|
544
|
+
const opening = openingOf(n);
|
|
545
|
+
if (!/^[a-z]/.test(opening.tagName.getText())) return false;
|
|
546
|
+
if (ts.isJsxElement(n) && n.children.some(c => !(ts.isJsxText(c) && !c.text.trim()))) return false;
|
|
547
|
+
return opening.attributes.properties.every(
|
|
548
|
+
a => ts.isJsxAttribute(a) && /^(?:className|style|key|aria-hidden|role)$/.test(a.name.getText()),
|
|
549
|
+
);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const isJsx = node => {
|
|
553
|
+
const n = strip(node);
|
|
554
|
+
return !!n && (ts.isJsxElement(n) || ts.isJsxSelfClosingElement(n) || ts.isJsxFragment(n));
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/** The name node a node renders: a JSX tag, or the first argument of `createElement` / `jsx`. Else null. */
|
|
558
|
+
function renderedTag(node) {
|
|
559
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) return node.tagName;
|
|
560
|
+
if (
|
|
561
|
+
ts.isCallExpression(node) &&
|
|
562
|
+
node.arguments[0] &&
|
|
563
|
+
/(?:^|\.)(?:createElement|jsx|jsxs|jsxDEV)$/.test(dottedName(node.expression))
|
|
564
|
+
)
|
|
565
|
+
return node.arguments[0];
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Whether a resolved tag is a skeleton (`XSkeleton`, `XSkeleton.Part`, `TextSkeleton.Body`). */
|
|
570
|
+
const isSkeletonName = tag => /Skeleton(?:\.\w+)?$/.test(tag);
|
|
571
|
+
/** Whether an attribute of an element is a skeleton count: a `*Rows`/`*Count` skeleton prop, `LoadingItems`' `count`, a `*Skeleton`'s `rows` / `count`. */
|
|
572
|
+
const isCountAttribute = (tag, name) =>
|
|
573
|
+
COUNT_ATTR.test(name) ||
|
|
574
|
+
(tag.split('.').pop() === 'LoadingItems' && name === 'count') ||
|
|
575
|
+
(isSkeletonName(tag) && /^(?:rows|count)$/.test(name));
|
|
576
|
+
/** Whether a function (or a name) is a skeleton's: `XSkeleton`, `renderRowSkeleton`. */
|
|
577
|
+
const isSkeletonFunctionName = name => /Skeleton$/.test(name);
|
|
578
|
+
/** Whether a name is a component skeleton's (`XSkeleton`, capitalised), the component being `X`. */
|
|
579
|
+
const isComponentSkeletonName = name => isComponentName(name) && name !== 'Skeleton' && isSkeletonFunctionName(name);
|
|
580
|
+
/** Whether a call is `next/dynamic`'s (`dynamic(…)`, `x.dynamic(…)`). */
|
|
581
|
+
const isDynamicCall = node => ts.isCallExpression(node) && /(?:^|\.)dynamic$/.test(dottedName(node.expression));
|
|
582
|
+
|
|
583
|
+
/** Whether a node sits in a `next/dynamic` call's `loading:` slot (the allowed fallback form). */
|
|
584
|
+
function inDynamicLoading(node) {
|
|
585
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
586
|
+
if (!ts.isPropertyAssignment(p) || p.name.getText() !== 'loading' || !ts.isObjectLiteralExpression(p.parent))
|
|
587
|
+
continue;
|
|
588
|
+
const call = p.parent.parent;
|
|
589
|
+
if (call && isDynamicCall(call)) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
595
|
+
// One file: its bindings, and the names its JSX resolves to
|
|
596
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
class FileModel {
|
|
599
|
+
constructor(project, path) {
|
|
600
|
+
this.project = project;
|
|
601
|
+
this.path = path;
|
|
602
|
+
this.sf = project.parse(path);
|
|
603
|
+
this.lines = this.sf.text.split('\n');
|
|
604
|
+
this.imports = new Map();
|
|
605
|
+
for (const st of this.sf.statements) {
|
|
606
|
+
if (!ts.isImportDeclaration(st) || !st.importClause) continue;
|
|
607
|
+
const spec = st.moduleSpecifier.text;
|
|
608
|
+
const file = project.resolveModule(path, spec);
|
|
609
|
+
const clause = st.importClause;
|
|
610
|
+
if (clause.name) this.imports.set(clause.name.text, { kind: 'import', imported: 'default', spec, file });
|
|
611
|
+
const named = clause.namedBindings;
|
|
612
|
+
if (named && ts.isNamespaceImport(named))
|
|
613
|
+
this.imports.set(named.name.text, { kind: 'namespace', imported: '*', spec, file });
|
|
614
|
+
if (named && ts.isNamedImports(named)) {
|
|
615
|
+
for (const el of named.elements) {
|
|
616
|
+
this.imports.set(el.name.text, {
|
|
617
|
+
kind: 'import',
|
|
618
|
+
imported: (el.propertyName ?? el.name).text,
|
|
619
|
+
spec,
|
|
620
|
+
file,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
binding(name) {
|
|
628
|
+
return this.imports.get(name) ?? null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** The declaration `name` refers to at `at`: a parameter, a local / module variable, a function, or an import. */
|
|
632
|
+
lookup(name, at) {
|
|
633
|
+
for (let scope = at; scope; scope = scope.parent) {
|
|
634
|
+
if (isFunctionLike(scope)) {
|
|
635
|
+
for (const param of scope.parameters) {
|
|
636
|
+
if (bindsName(param.name, name)) return { kind: 'param', node: param, fn: scope };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const statements =
|
|
640
|
+
ts.isBlock(scope) || ts.isSourceFile(scope) || ts.isModuleBlock(scope) ? scope.statements : null;
|
|
641
|
+
if (!statements) continue;
|
|
642
|
+
for (const st of statements) {
|
|
643
|
+
if (ts.isVariableStatement(st)) {
|
|
644
|
+
for (const d of st.declarationList.declarations) {
|
|
645
|
+
if (bindsName(d.name, name))
|
|
646
|
+
return { kind: 'var', node: d, init: d.initializer, destructured: !ts.isIdentifier(d.name) };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if ((ts.isFunctionDeclaration(st) || ts.isClassDeclaration(st)) && st.name?.text === name)
|
|
650
|
+
return { kind: 'function', node: st };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const imp = this.binding(name);
|
|
654
|
+
return imp ? { ...imp } : null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** The name an identifier stands for (through an import rename or a local alias), e.g. `Bars` -> `ChildSkeleton`. */
|
|
658
|
+
canonical(name, at, depth = 0) {
|
|
659
|
+
const decl = this.lookup(name, at);
|
|
660
|
+
if (!decl) return name;
|
|
661
|
+
if (decl.kind === 'import') return decl.imported === 'default' ? name : decl.imported;
|
|
662
|
+
if (decl.kind === 'var' && !decl.destructured && decl.init && depth < 4) {
|
|
663
|
+
const n = strip(decl.init);
|
|
664
|
+
if (ts.isIdentifier(n)) return this.canonical(n.text, decl.node, depth + 1);
|
|
665
|
+
if (ts.isPropertyAccessExpression(n) && this.binding(dottedName(n.expression))?.kind === 'namespace')
|
|
666
|
+
return n.name.text;
|
|
667
|
+
}
|
|
668
|
+
return name;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** The name a tag / callee resolves to (`NS.X` through a namespace import is `X`; `A.B` stays dotted). */
|
|
672
|
+
resolveName(node) {
|
|
673
|
+
const n = strip(node);
|
|
674
|
+
if (!n) return '';
|
|
675
|
+
if (ts.isIdentifier(n)) return this.canonical(n.text, n);
|
|
676
|
+
if (ts.isPropertyAccessExpression(n)) {
|
|
677
|
+
const left = dottedName(n.expression);
|
|
678
|
+
if (this.binding(left)?.kind === 'namespace') return n.name.text;
|
|
679
|
+
const leftResolved = ts.isIdentifier(strip(n.expression)) ? this.canonical(left, n) : left;
|
|
680
|
+
return `${leftResolved}.${n.name.text}`;
|
|
681
|
+
}
|
|
682
|
+
return dottedName(n);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
line(node) {
|
|
686
|
+
return this.sf.getLineAndCharacterOfPosition(node.getStart(this.sf)).line + 1;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The file's opt-out markers (see the header): `{ line, rule, why, covers, used }`, `rule` null when the marker is
|
|
691
|
+
* not the rule-scoped form. A marker covers its own line, and the next one when it stands alone on its line.
|
|
692
|
+
*/
|
|
693
|
+
get markers() {
|
|
694
|
+
if (!this._markers) {
|
|
695
|
+
this._markers = [];
|
|
696
|
+
this.lines.forEach((text, index) => {
|
|
697
|
+
if (!OPT_OUT_ANY.test(text)) return;
|
|
698
|
+
const mark = OPT_OUT_MARK.exec(text);
|
|
699
|
+
const alone = /^\s*(?:\/\/|\/\*|\{\s*\/\*)/.test(text);
|
|
700
|
+
const line = index + 1;
|
|
701
|
+
this._markers.push({
|
|
702
|
+
line,
|
|
703
|
+
rule: mark?.[1] || null,
|
|
704
|
+
why: mark ? mark[2].split('*/')[0].trim() : '',
|
|
705
|
+
covers: alone ? [line, line + 1] : [line],
|
|
706
|
+
used: false,
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return this._markers;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Whether a marker lifts `rule` at `node`'s line (and records that it did). */
|
|
714
|
+
optedOut(node, rule) {
|
|
715
|
+
const line = this.line(node);
|
|
716
|
+
const marker = this.markers.find(m => m.rule === rule && m.why && m.covers.includes(line));
|
|
717
|
+
if (marker) marker.used = true;
|
|
718
|
+
return !!marker;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function bindsName(bindingName, name) {
|
|
723
|
+
if (ts.isIdentifier(bindingName)) return bindingName.text === name;
|
|
724
|
+
if (ts.isObjectBindingPattern(bindingName) || ts.isArrayBindingPattern(bindingName)) {
|
|
725
|
+
return bindingName.elements.some(el => !ts.isOmittedExpression(el) && bindsName(el.name, name));
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
731
|
+
// The project: files, the declaration index, module resolution
|
|
732
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* A project: `trees` are `{ dir, report, sources? }` (report: findings are collected for it; else it is only indexed, as
|
|
736
|
+
* the installed lib is for a host). `@/` resolves to the tree's own `dir`; a package import resolves by name through
|
|
737
|
+
* the declaration index.
|
|
738
|
+
*/
|
|
739
|
+
export class Project {
|
|
740
|
+
/** A tree's `sources` (path -> text) replaces reading it from disk (a fixture, a single file). */
|
|
741
|
+
constructor({ trees } = {}) {
|
|
742
|
+
this.trees = trees.map(t => ({ ...t, dir: resolve(t.dir) }));
|
|
743
|
+
this.text = new Map();
|
|
744
|
+
this.treeOf = new Map();
|
|
745
|
+
for (const tree of this.trees) {
|
|
746
|
+
const entries = tree.sources
|
|
747
|
+
? [...tree.sources].map(([path, text]) => [resolve(path), text])
|
|
748
|
+
: [...walkSourceFiles(tree.dir)].map(path => [path, null]);
|
|
749
|
+
for (const [path, given] of entries) {
|
|
750
|
+
if (this.text.has(path)) continue;
|
|
751
|
+
this.text.set(path, given ?? readFileSync(path, 'utf8'));
|
|
752
|
+
this.treeOf.set(path, tree);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.parsed = new Map();
|
|
756
|
+
this.declaredIn = new Map();
|
|
757
|
+
for (const [path, text] of this.text) {
|
|
758
|
+
for (const m of text.matchAll(/\b(?:function\s*\*?|const|let|class)\s+([A-Z][\w$]*)\b/g)) {
|
|
759
|
+
if (!this.declaredIn.has(m[1])) this.declaredIn.set(m[1], new Set());
|
|
760
|
+
this.declaredIn.get(m[1]).add(path);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
this.hooksDeclared = new Set();
|
|
764
|
+
for (const text of this.text.values()) {
|
|
765
|
+
for (const m of text.matchAll(/\b(?:function|const|let)\s+(use[A-Z][\w$]*)\b/g)) this.hooksDeclared.add(m[1]);
|
|
766
|
+
}
|
|
767
|
+
this.switchCache = new Map();
|
|
768
|
+
this.marksCache = new Map();
|
|
769
|
+
this.scopeHooks = this.#hooksCalling(new Set([SCOPE_HOOK]), text => text.includes(SCOPE_HOOK));
|
|
770
|
+
this.dataHooks = this.#hooksCalling(
|
|
771
|
+
new Set(),
|
|
772
|
+
text => /\b(?:function|const|let)\s+use[A-Z]/.test(text),
|
|
773
|
+
QUERY_HOOK,
|
|
774
|
+
);
|
|
775
|
+
this.suspendingHooks = this.#hooksCalling(
|
|
776
|
+
new Set(),
|
|
777
|
+
text => /\b(?:useLazyLoadQuery|usePreloadedQuery|useSuspenseQuery|useSuspenseQueries)\b/.test(text),
|
|
778
|
+
SUSPENDING_READ,
|
|
779
|
+
);
|
|
780
|
+
this.deprecated = this.#deprecatedSkeletons();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Whether a hook returns read data: it calls a query (directly or through a data hook), or is not declared here. */
|
|
784
|
+
isDataHook(name) {
|
|
785
|
+
if (!/^use[A-Z]/.test(name) || NOT_DATA_HOOK.test(name) || this.scopeHooks.has(name) || LOCAL_HOOK.test(name))
|
|
786
|
+
return false;
|
|
787
|
+
return QUERY_HOOK.test(name) || this.dataHooks.has(name) || !this.hooksDeclared.has(name);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
parse(path) {
|
|
791
|
+
let sf = this.parsed.get(path);
|
|
792
|
+
if (!sf) {
|
|
793
|
+
const text = this.text.get(path) ?? readFileSync(path, 'utf8');
|
|
794
|
+
sf = ts.createSourceFile(
|
|
795
|
+
path,
|
|
796
|
+
text,
|
|
797
|
+
ts.ScriptTarget.Latest,
|
|
798
|
+
true,
|
|
799
|
+
path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
800
|
+
);
|
|
801
|
+
this.parsed.set(path, sf);
|
|
802
|
+
}
|
|
803
|
+
return sf;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* The `use*` hooks whose body calls one of `hooks` (or a callee matching `seed`), to a fixed point: the scope hooks
|
|
808
|
+
* (`useLoadingScope` and every hook calling it) and the data hooks (every hook calling a query).
|
|
809
|
+
*/
|
|
810
|
+
#hooksCalling(hooks, candidate, seed = null) {
|
|
811
|
+
const paths = [...this.text].filter(([, text]) => candidate(text)).map(([path]) => path);
|
|
812
|
+
for (let round = 0; round < 4; round++) {
|
|
813
|
+
const before = hooks.size;
|
|
814
|
+
for (const path of paths) {
|
|
815
|
+
walk(this.parse(path), n => {
|
|
816
|
+
if (!isFunctionLike(n)) return undefined;
|
|
817
|
+
const name = functionName(n);
|
|
818
|
+
if (/^use[A-Z]/.test(name) && !hooks.has(name) && this.#callsHook(n, hooks, seed)) hooks.add(name);
|
|
819
|
+
return undefined;
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (hooks.size === before) break;
|
|
823
|
+
}
|
|
824
|
+
return hooks;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
#callsHook(fn, hooks, seed = null) {
|
|
828
|
+
let found = false;
|
|
829
|
+
const body = bodyOf(fn);
|
|
830
|
+
if (!body) return false;
|
|
831
|
+
walk(body, n => {
|
|
832
|
+
if (found) return false;
|
|
833
|
+
if (n !== fn && isFunctionLike(n) && isComponentName(functionName(n))) return false;
|
|
834
|
+
if (ts.isCallExpression(n)) {
|
|
835
|
+
const callee = dottedName(n.expression).split('.').pop();
|
|
836
|
+
if (hooks.has(callee) || seed?.test(callee)) found = true;
|
|
837
|
+
if (!seed && callee === 'useContext' && /LoadingScope/.test(n.arguments[0]?.getText() ?? '')) found = true;
|
|
838
|
+
}
|
|
839
|
+
return undefined;
|
|
840
|
+
});
|
|
841
|
+
return found;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Whether a function calls the loading scope (directly or through a hook that does). */
|
|
845
|
+
callsScope(fn) {
|
|
846
|
+
return this.#callsHook(fn, this.scopeHooks);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** The `*Skeleton` components the indexed source marks `@deprecated` (the JSDoc above their export). */
|
|
850
|
+
#deprecatedSkeletons() {
|
|
851
|
+
const names = new Set();
|
|
852
|
+
for (const text of this.text.values()) {
|
|
853
|
+
if (!text.includes('@deprecated')) continue;
|
|
854
|
+
for (const m of text.matchAll(
|
|
855
|
+
/\/\*\*((?:(?!\*\/)[\s\S])*)\*\/\s*(?:\/\/[^\n]*\n\s*)*export\s+(?:function|const)\s+([A-Z][A-Za-z0-9]*Skeleton)\b/g,
|
|
856
|
+
)) {
|
|
857
|
+
if (m[1].includes('@deprecated')) names.add(m[2]);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return names;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** The file an import specifier points to, from `fromPath` (relative or `@/`), or null for a package. */
|
|
864
|
+
resolveModule(fromPath, spec) {
|
|
865
|
+
const tree = this.treeOf.get(fromPath);
|
|
866
|
+
let base = null;
|
|
867
|
+
if (spec.startsWith('@/') && tree) base = join(tree.dir, spec.slice(2));
|
|
868
|
+
else if (spec.startsWith('.')) base = resolve(dirname(fromPath), spec);
|
|
869
|
+
if (!base) return null;
|
|
870
|
+
for (const candidate of [base, `${base}.tsx`, `${base}.ts`, join(base, 'index.tsx'), join(base, 'index.ts')]) {
|
|
871
|
+
if (/\.tsx?$/.test(candidate) && this.text.has(candidate)) return candidate;
|
|
872
|
+
if (/\.tsx?$/.test(candidate) && existsSync(candidate) && statSync(candidate).isFile()) {
|
|
873
|
+
this.text.set(candidate, readFileSync(candidate, 'utf8'));
|
|
874
|
+
if (tree) this.treeOf.set(candidate, { ...tree, report: false });
|
|
875
|
+
return candidate;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/** The top-level declaration of `name` in `path` (a function, or a variable's initializer), or null. */
|
|
882
|
+
declarationIn(path, name) {
|
|
883
|
+
const sf = this.parse(path);
|
|
884
|
+
for (const st of sf.statements) {
|
|
885
|
+
if (ts.isFunctionDeclaration(st) && st.name?.text === name) return { kind: 'function', node: st };
|
|
886
|
+
if (ts.isClassDeclaration(st) && st.name?.text === name) return { kind: 'class', node: st };
|
|
887
|
+
if (ts.isVariableStatement(st)) {
|
|
888
|
+
for (const d of st.declarationList.declarations) {
|
|
889
|
+
if (ts.isIdentifier(d.name) && d.name.text === name) return { kind: 'var', node: d, init: d.initializer };
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** Whether component `name` switches itself (its function reads the loading scope), in any module declaring it. */
|
|
897
|
+
componentSwitches(name, depth = 0) {
|
|
898
|
+
if (this.switchCache.has(name)) return this.switchCache.get(name);
|
|
899
|
+
this.switchCache.set(name, false);
|
|
900
|
+
let result = false;
|
|
901
|
+
for (const path of this.declaredIn.get(name) ?? []) {
|
|
902
|
+
if (this.#declarationSwitches(path, name, depth)) {
|
|
903
|
+
result = true;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
this.switchCache.set(name, result);
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#declarationSwitches(path, name, depth) {
|
|
912
|
+
const decl = this.declarationIn(path, name);
|
|
913
|
+
if (!decl) return /\buseLoadingScope\s*\(/.test(this.text.get(path) ?? '');
|
|
914
|
+
let fn = null;
|
|
915
|
+
if (decl.kind === 'function') fn = decl.node;
|
|
916
|
+
else if (decl.kind === 'var' && decl.init) fn = functionInside(decl.init);
|
|
917
|
+
if (!fn) {
|
|
918
|
+
// `const Y = X` / `Object.assign(X, {...})`: follow X.
|
|
919
|
+
const target = decl.init ? aliasTarget(decl.init) : null;
|
|
920
|
+
return target && depth < 3 && target !== name ? this.componentSwitches(target, depth + 1) : false;
|
|
921
|
+
}
|
|
922
|
+
if (this.callsScope(fn)) return true;
|
|
923
|
+
// Delegation: the function returns a component that switches, handing it the loading state (`loading` or a spread).
|
|
924
|
+
let delegated = false;
|
|
925
|
+
walk(bodyOf(fn) ?? fn, n => {
|
|
926
|
+
if (delegated) return false;
|
|
927
|
+
if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
|
|
928
|
+
const tag = dottedName(n.tagName);
|
|
929
|
+
if (tag === 'LoadingWhen') delegated = true;
|
|
930
|
+
// Only the component's own props spread hand it the loading state (`<Link {...anchorProps}>` does not).
|
|
931
|
+
const ownProps = new Set(
|
|
932
|
+
fn.parameters.flatMap(param =>
|
|
933
|
+
ts.isIdentifier(param.name)
|
|
934
|
+
? [param.name.text]
|
|
935
|
+
: ts.isObjectBindingPattern(param.name)
|
|
936
|
+
? param.name.elements.filter(e => e.dotDotDotToken && ts.isIdentifier(e.name)).map(e => e.name.text)
|
|
937
|
+
: [],
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
const handsLoading = n.attributes.properties.some(
|
|
941
|
+
a =>
|
|
942
|
+
(ts.isJsxSpreadAttribute(a) && ownProps.has(a.expression.getText())) ||
|
|
943
|
+
(ts.isJsxAttribute(a) && /^(?:loading|isLoading)$/.test(a.name.getText())),
|
|
944
|
+
);
|
|
945
|
+
const target = tag.split('.').pop();
|
|
946
|
+
if (handsLoading && depth < 3 && isComponentName(target) && target !== name)
|
|
947
|
+
delegated = this.componentSwitches(target, depth + 1);
|
|
948
|
+
}
|
|
949
|
+
return undefined;
|
|
950
|
+
});
|
|
951
|
+
return delegated;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Whether component `local` (as named in `fromPath`) carries the `kind` mark (`item`: `data-loading-item`,
|
|
956
|
+
* `loadingItemProps(…)`, `listItem(s)`; `optional`: `data-loading-optional`) on an element it renders, read from its
|
|
957
|
+
* declaration's AST through its import (a comment or a string that mentions the mark is not one).
|
|
958
|
+
*/
|
|
959
|
+
componentMarks(fromPath, local, kind, depth = 0) {
|
|
960
|
+
const key = `${fromPath}\0${local}\0${kind}`;
|
|
961
|
+
if (this.marksCache.has(key)) return this.marksCache.get(key);
|
|
962
|
+
this.marksCache.set(key, false);
|
|
963
|
+
const model = this.model(fromPath);
|
|
964
|
+
const binding = model.binding(local);
|
|
965
|
+
let targets = [];
|
|
966
|
+
if (binding?.kind === 'import') {
|
|
967
|
+
const files = binding.file ? [binding.file] : [...(this.declaredIn.get(binding.imported) ?? [])];
|
|
968
|
+
targets = files.map(path => [
|
|
969
|
+
path,
|
|
970
|
+
binding.imported === 'default' ? this.#defaultExport(path) : binding.imported,
|
|
971
|
+
]);
|
|
972
|
+
} else targets = [[fromPath, local]];
|
|
973
|
+
const result = targets.some(([path, name]) => {
|
|
974
|
+
const decl = name && this.declarationIn(path, name);
|
|
975
|
+
if (!decl) return false;
|
|
976
|
+
const fn = decl.kind === 'function' ? decl.node : decl.init ? (functionInside(decl.init) ?? decl.init) : null;
|
|
977
|
+
if (!fn) return false;
|
|
978
|
+
const target = !isFunctionLike(fn) && decl.init ? aliasTarget(decl.init) : null;
|
|
979
|
+
if (target && depth < 3) return this.componentMarks(path, target, kind, depth + 1);
|
|
980
|
+
return this.#marksIn(fn, path, kind, depth);
|
|
981
|
+
});
|
|
982
|
+
this.marksCache.set(key, result);
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
#marksIn(node, path, kind, depth) {
|
|
987
|
+
const attr = kind === 'item' ? ITEM_MARK_ATTR : /^data-loading-optional$/;
|
|
988
|
+
let hit = false;
|
|
989
|
+
walk(node, n => {
|
|
990
|
+
if (hit) return false;
|
|
991
|
+
if (ts.isJsxAttribute(n) && attr.test(n.name.getText())) hit = true;
|
|
992
|
+
if (kind === 'item' && ts.isCallExpression(n) && /(?:^|\.)loadingItemProps$/.test(dottedName(n.expression)))
|
|
993
|
+
hit = true;
|
|
994
|
+
// An element it renders whose own component carries the mark (`<Row {...props} />` → Row's root).
|
|
995
|
+
const tag = renderedTag(n);
|
|
996
|
+
if (!hit && tag && depth < 3) {
|
|
997
|
+
const name = dottedName(tag);
|
|
998
|
+
if (isComponentName(name) && this.componentMarks(path, name.split('.')[0], kind, depth + 1)) hit = true;
|
|
999
|
+
}
|
|
1000
|
+
return undefined;
|
|
1001
|
+
});
|
|
1002
|
+
return hit;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** Whether component `local` (as named in `fromPath`) renders nothing: every return of its declaration is nullish. */
|
|
1006
|
+
rendersNothing(fromPath, local) {
|
|
1007
|
+
const binding = this.model(fromPath).binding(local);
|
|
1008
|
+
const targets =
|
|
1009
|
+
binding?.kind === 'import'
|
|
1010
|
+
? (binding.file ? [binding.file] : [...(this.declaredIn.get(binding.imported) ?? [])]).map(path => [
|
|
1011
|
+
path,
|
|
1012
|
+
binding.imported === 'default' ? this.#defaultExport(path) : binding.imported,
|
|
1013
|
+
])
|
|
1014
|
+
: [[fromPath, local]];
|
|
1015
|
+
return targets.some(([path, name]) => {
|
|
1016
|
+
const decl = name && this.declarationIn(path, name);
|
|
1017
|
+
const fn = decl?.kind === 'function' ? decl.node : decl?.init ? functionInside(decl.init) : null;
|
|
1018
|
+
if (!fn?.body) return false;
|
|
1019
|
+
if (!ts.isBlock(fn.body)) return isNullish(fn.body);
|
|
1020
|
+
let returns = 0;
|
|
1021
|
+
let nothing = true;
|
|
1022
|
+
walk(fn.body, n => {
|
|
1023
|
+
if (isFunctionLike(n)) return false;
|
|
1024
|
+
if (ts.isReturnStatement(n)) {
|
|
1025
|
+
returns++;
|
|
1026
|
+
if (n.expression && !isNullish(n.expression)) nothing = false;
|
|
1027
|
+
}
|
|
1028
|
+
return undefined;
|
|
1029
|
+
});
|
|
1030
|
+
return returns > 0 && nothing;
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** The name a module's default export is declared by, or null. */
|
|
1035
|
+
#defaultExport(path) {
|
|
1036
|
+
for (const st of this.parse(path).statements) {
|
|
1037
|
+
if (
|
|
1038
|
+
(ts.isFunctionDeclaration(st) || ts.isClassDeclaration(st)) &&
|
|
1039
|
+
st.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)
|
|
1040
|
+
)
|
|
1041
|
+
return st.name?.text ?? null;
|
|
1042
|
+
if (ts.isExportAssignment(st) && ts.isIdentifier(strip(st.expression))) return strip(st.expression).text;
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
model(path) {
|
|
1048
|
+
if (!this.models) this.models = new Map();
|
|
1049
|
+
let m = this.models.get(path);
|
|
1050
|
+
if (!m) {
|
|
1051
|
+
m = new FileModel(this, path);
|
|
1052
|
+
this.models.set(path, m);
|
|
1053
|
+
}
|
|
1054
|
+
return m;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** A `{…}` child of an element (not an attribute's value). */
|
|
1059
|
+
function isJsxChild(node) {
|
|
1060
|
+
return ts.isJsxExpression(node) && !ts.isJsxAttribute(node.parent);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function functionInside(init) {
|
|
1064
|
+
const n = strip(init);
|
|
1065
|
+
if (!n) return null;
|
|
1066
|
+
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return n;
|
|
1067
|
+
if (ts.isCallExpression(n) && WRAPPER_CALL.test(dottedName(n.expression))) {
|
|
1068
|
+
for (const arg of n.arguments) {
|
|
1069
|
+
const fn = functionInside(arg);
|
|
1070
|
+
if (fn) return fn;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function aliasTarget(init) {
|
|
1077
|
+
const n = strip(init);
|
|
1078
|
+
if (ts.isIdentifier(n)) return n.text;
|
|
1079
|
+
if (ts.isPropertyAccessExpression(n)) return n.name.text;
|
|
1080
|
+
if (ts.isCallExpression(n) && dottedName(n.expression) === 'Object.assign' && n.arguments[0])
|
|
1081
|
+
return aliasTarget(n.arguments[0]);
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1086
|
+
// Conditions
|
|
1087
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1088
|
+
|
|
1089
|
+
/** The last name of an identifier or a property access (`query.isLoading` -> `isLoading`). */
|
|
1090
|
+
function leafName(node) {
|
|
1091
|
+
const n = strip(node);
|
|
1092
|
+
if (!n) return '';
|
|
1093
|
+
if (ts.isIdentifier(n)) return n.text;
|
|
1094
|
+
if (ts.isPropertyAccessExpression(n)) return n.name.text;
|
|
1095
|
+
return '';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function isLoadingFlag(node, model) {
|
|
1099
|
+
const n = strip(node);
|
|
1100
|
+
if (!n) return false;
|
|
1101
|
+
if ((ts.isIdentifier(n) || ts.isPropertyAccessExpression(n)) && LOADING_FLAG.test(leafName(n))) return true;
|
|
1102
|
+
// A local that holds the scope (`const scoped = useLoadingScope()`, `const busy = useLoadingScope() || x`).
|
|
1103
|
+
if (ts.isIdentifier(n) && model) {
|
|
1104
|
+
const decl = model.lookup(n.text, n);
|
|
1105
|
+
if (decl?.kind === 'var' && decl.init && !decl.destructured && readsScope(decl.init, model)) return true;
|
|
1106
|
+
}
|
|
1107
|
+
if (ts.isCallExpression(n) && model?.project.scopeHooks.has(dottedName(n.expression).split('.').pop())) return true;
|
|
1108
|
+
if (
|
|
1109
|
+
ts.isBinaryExpression(n) &&
|
|
1110
|
+
[ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken].includes(n.operatorToken.kind)
|
|
1111
|
+
) {
|
|
1112
|
+
const lit = [n.left, n.right].map(strip).find(x => ts.isStringLiteral(x));
|
|
1113
|
+
return !!lit && /^(?:loading|pending)$/.test(lit.text);
|
|
1114
|
+
}
|
|
1115
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
1116
|
+
return isLoadingFlag(n.left, model) || isLoadingFlag(n.right, model);
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Whether an expression calls the loading scope (a scope hook), directly or as an operand of `||`. */
|
|
1121
|
+
function readsScope(expr, model) {
|
|
1122
|
+
const n = strip(expr);
|
|
1123
|
+
if (ts.isCallExpression(n)) return model.project.scopeHooks.has(dottedName(n.expression).split('.').pop());
|
|
1124
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
1125
|
+
return readsScope(n.left, model) || readsScope(n.right, model);
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function isNegated(node) {
|
|
1130
|
+
const n = strip(node);
|
|
1131
|
+
return !!n && ts.isPrefixUnaryExpression(n) && n.operator === ts.SyntaxKind.ExclamationToken;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function isNegatedLoading(node, model) {
|
|
1135
|
+
const n = strip(node);
|
|
1136
|
+
if (isNegated(n)) return isLoadingFlag(n.operand, model);
|
|
1137
|
+
if (
|
|
1138
|
+
ts.isBinaryExpression(n) &&
|
|
1139
|
+
[ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken].includes(n.operatorToken.kind)
|
|
1140
|
+
) {
|
|
1141
|
+
const lit = [n.left, n.right].map(strip).find(x => ts.isStringLiteral(x));
|
|
1142
|
+
return !!lit && /^(?:loading|pending)$/.test(lit.text);
|
|
1143
|
+
}
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** The root identifier of a property chain (`a?.b.c` -> `a`), or null. */
|
|
1148
|
+
function chainRoot(node) {
|
|
1149
|
+
let n = strip(node);
|
|
1150
|
+
while (n && (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n))) n = strip(n.expression);
|
|
1151
|
+
return n && ts.isIdentifier(n) ? n : null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Whether an expression reads a query's data (`data`, `xData`, `x.data`, a field of them). */
|
|
1155
|
+
function isReadData(node, model, { hookOnly = false } = {}) {
|
|
1156
|
+
const n = strip(node);
|
|
1157
|
+
if (!n) return false;
|
|
1158
|
+
if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
|
|
1159
|
+
if (ts.isPropertyAccessExpression(n) && n.name.text === 'data') return true;
|
|
1160
|
+
// A flag or an action of a hook's result (`test.showResults`, `q.isLoading`) is not its data.
|
|
1161
|
+
if (ts.isPropertyAccessExpression(n) && (FLAG_FIELD.test(n.name.text) || NOT_DATA_FIELD.test(n.name.text)))
|
|
1162
|
+
return false;
|
|
1163
|
+
return isReadData(n.expression, model, { hookOnly });
|
|
1164
|
+
}
|
|
1165
|
+
if (!ts.isIdentifier(n)) return false;
|
|
1166
|
+
const decl = model.lookup(n.text, n);
|
|
1167
|
+
const fromHook =
|
|
1168
|
+
decl?.kind === 'var' &&
|
|
1169
|
+
decl.init &&
|
|
1170
|
+
ts.isCallExpression(strip(decl.init)) &&
|
|
1171
|
+
/^use[A-Z]/.test(dottedName(strip(decl.init).expression).split('.').pop());
|
|
1172
|
+
if (decl?.kind === 'var' && decl.init && readOrigin(n.text, decl, model)) return true;
|
|
1173
|
+
if (!READ_DATA.test(n.text)) return false;
|
|
1174
|
+
const derived =
|
|
1175
|
+
decl?.kind === 'var' &&
|
|
1176
|
+
decl.init &&
|
|
1177
|
+
(!ts.isCallExpression(strip(decl.init)) || LOCAL_HOOK.test(dottedName(strip(decl.init).expression)));
|
|
1178
|
+
if (derived) return false;
|
|
1179
|
+
if (hookOnly) return fromHook || !decl;
|
|
1180
|
+
return n.text === 'data' || fromHook || !decl || decl.kind === 'param';
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Whether a local holds READ DATA by where it comes from: what a data hook returns (`const { tickets } =
|
|
1185
|
+
* useTickets()`, `const user = useUser()`; not a flag, an error or an action of it), or an alias of read data
|
|
1186
|
+
* (`const user = query.data`).
|
|
1187
|
+
*/
|
|
1188
|
+
function readOrigin(name, decl, model, depth = 0) {
|
|
1189
|
+
const init = strip(decl.init);
|
|
1190
|
+
if (ts.isCallExpression(init)) {
|
|
1191
|
+
const hook = dottedName(init.expression).split('.').pop();
|
|
1192
|
+
if (!model.project.isDataHook(hook)) return false;
|
|
1193
|
+
if (!decl.destructured) return true;
|
|
1194
|
+
const el = bindingElement(decl.node.name, name);
|
|
1195
|
+
const field = el ? (el.propertyName ?? el.name).getText() : name;
|
|
1196
|
+
return !LOADING_FLAG.test(field) && !NOT_DATA_FIELD.test(field) && !el?.initializer && !el?.dotDotDotToken;
|
|
1197
|
+
}
|
|
1198
|
+
if (decl.destructured || depth > 3) return false;
|
|
1199
|
+
return (
|
|
1200
|
+
(ts.isPropertyAccessExpression(init) || ts.isElementAccessExpression(init) || ts.isIdentifier(init)) &&
|
|
1201
|
+
isReadData(init, model)
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** The binding element that binds `name` in a destructuring pattern, or null. */
|
|
1206
|
+
function bindingElement(pattern, name) {
|
|
1207
|
+
if (!ts.isObjectBindingPattern(pattern) && !ts.isArrayBindingPattern(pattern)) return null;
|
|
1208
|
+
for (const el of pattern.elements) {
|
|
1209
|
+
if (ts.isOmittedExpression(el)) continue;
|
|
1210
|
+
if (ts.isIdentifier(el.name) && el.name.text === name) return el;
|
|
1211
|
+
const inner = bindingElement(el.name, name);
|
|
1212
|
+
if (inner) return inner;
|
|
1213
|
+
}
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** True when the condition holds WHILE the read is pending (loading, or the data missing). */
|
|
1218
|
+
function heldWhileLoading(test, model, opts) {
|
|
1219
|
+
const n = strip(test);
|
|
1220
|
+
if (!n) return false;
|
|
1221
|
+
if (isLoadingFlag(n, model)) return true;
|
|
1222
|
+
if (isNegated(n) && isReadData(n.operand, model, opts)) return true;
|
|
1223
|
+
if (ts.isBinaryExpression(n)) {
|
|
1224
|
+
const op = n.operatorToken.kind;
|
|
1225
|
+
if (op === ts.SyntaxKind.BarBarToken)
|
|
1226
|
+
return heldWhileLoading(n.left, model, opts) || heldWhileLoading(n.right, model, opts);
|
|
1227
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
1228
|
+
return (
|
|
1229
|
+
(heldWhileLoading(n.left, model, opts) || heldWhileLoading(n.right, model, opts)) &&
|
|
1230
|
+
!isNegatedLoading(n.left, model) &&
|
|
1231
|
+
!isNegatedLoading(n.right, model)
|
|
1232
|
+
);
|
|
1233
|
+
if (
|
|
1234
|
+
[ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(op) &&
|
|
1235
|
+
[n.left, n.right].some(isNullish)
|
|
1236
|
+
)
|
|
1237
|
+
return [n.left, n.right].some(x => !isNullish(x) && isReadData(x, model, opts));
|
|
1238
|
+
}
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** True when the condition is FALSE while the read is pending (`!loading`, `data`, `data?.items`). */
|
|
1243
|
+
function falseWhileLoading(test, model, opts) {
|
|
1244
|
+
const n = strip(test);
|
|
1245
|
+
if (!n) return false;
|
|
1246
|
+
if (isNegatedLoading(n, model)) return true;
|
|
1247
|
+
if (isReadData(n, model, opts)) return true;
|
|
1248
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
1249
|
+
return falseWhileLoading(n.left, model, opts) || falseWhileLoading(n.right, model, opts);
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** The operands of an `&&` chain (`a && b && c` -> [a, b, c]). */
|
|
1254
|
+
function conjuncts(node) {
|
|
1255
|
+
const n = strip(node);
|
|
1256
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
1257
|
+
return [...conjuncts(n.left), ...conjuncts(n.right)];
|
|
1258
|
+
return [n];
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/** Whether, inside `fn` and before `node`, the read has answered: a suspending read, a loading branch, the scope. */
|
|
1262
|
+
function answeredBefore(fn, node, model) {
|
|
1263
|
+
if (!fn?.body || !ts.isBlock(fn.body)) return false;
|
|
1264
|
+
const pos = node.getStart();
|
|
1265
|
+
let answered = false;
|
|
1266
|
+
walk(fn.body, n => {
|
|
1267
|
+
if (answered || n.getStart() >= pos) return false;
|
|
1268
|
+
if (n !== fn && isFunctionLike(n)) return false;
|
|
1269
|
+
if (ts.isCallExpression(n)) {
|
|
1270
|
+
const callee = dottedName(n.expression).split('.').pop();
|
|
1271
|
+
if (
|
|
1272
|
+
SUSPENDING_READ.test(callee) ||
|
|
1273
|
+
model.project.suspendingHooks.has(callee) ||
|
|
1274
|
+
model.project.scopeHooks.has(callee)
|
|
1275
|
+
)
|
|
1276
|
+
answered = true;
|
|
1277
|
+
}
|
|
1278
|
+
if (
|
|
1279
|
+
ts.isIfStatement(n) &&
|
|
1280
|
+
n.getEnd() <= pos &&
|
|
1281
|
+
isLoadingFlag(n.expression, model) &&
|
|
1282
|
+
containsReturn(n.thenStatement)
|
|
1283
|
+
)
|
|
1284
|
+
answered = true;
|
|
1285
|
+
return undefined;
|
|
1286
|
+
});
|
|
1287
|
+
return answered;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function containsReturn(node) {
|
|
1291
|
+
if (ts.isReturnStatement(node)) return true;
|
|
1292
|
+
let hit = false;
|
|
1293
|
+
walk(node, n => {
|
|
1294
|
+
if (isFunctionLike(n)) return false;
|
|
1295
|
+
if (ts.isReturnStatement(n)) hit = true;
|
|
1296
|
+
return undefined;
|
|
1297
|
+
});
|
|
1298
|
+
return hit;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/** Whether `node` is drawn only while loading (a loading branch, a `*Skeleton` function, `LoadingItems`' child). */
|
|
1302
|
+
function inLoadingContext(node, model) {
|
|
1303
|
+
let child = node;
|
|
1304
|
+
for (let p = node.parent; p; child = p, p = p.parent) {
|
|
1305
|
+
if (ts.isConditionalExpression(p)) {
|
|
1306
|
+
if (child === p.whenTrue && isLoadingFlag(p.condition, model)) return true;
|
|
1307
|
+
if (child === p.whenFalse && isNegatedLoading(p.condition, model)) return true;
|
|
1308
|
+
}
|
|
1309
|
+
if (
|
|
1310
|
+
ts.isBinaryExpression(p) &&
|
|
1311
|
+
p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
1312
|
+
child === p.right
|
|
1313
|
+
) {
|
|
1314
|
+
if (isLoadingFlag(p.left, model)) return true;
|
|
1315
|
+
}
|
|
1316
|
+
if (ts.isIfStatement(p) && child === p.thenStatement && isLoadingFlag(p.expression, model)) return true;
|
|
1317
|
+
if (ts.isJsxElement(p) && model.resolveName(p.openingElement.tagName) === 'LoadingItems') return true;
|
|
1318
|
+
if (isFunctionLike(p) && isSkeletonFunctionName(functionName(p))) return true;
|
|
1319
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1324
|
+
// Counts
|
|
1325
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1326
|
+
|
|
1327
|
+
const COUNT_ORDER = ['ok', 'unnamed', 'typed', 'cap'];
|
|
1328
|
+
const worst = list => list.reduce((a, b) => (COUNT_ORDER.indexOf(b) > COUNT_ORDER.indexOf(a) ? b : a), 'ok');
|
|
1329
|
+
|
|
1330
|
+
const ARITHMETIC = new Set([
|
|
1331
|
+
ts.SyntaxKind.PlusToken,
|
|
1332
|
+
ts.SyntaxKind.MinusToken,
|
|
1333
|
+
ts.SyntaxKind.AsteriskToken,
|
|
1334
|
+
ts.SyntaxKind.SlashToken,
|
|
1335
|
+
ts.SyntaxKind.PercentToken,
|
|
1336
|
+
]);
|
|
1337
|
+
|
|
1338
|
+
const EQUALITY = new Set([
|
|
1339
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
1340
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
1341
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
1342
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
1343
|
+
]);
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* What a count expression is: 'typed' (a literal), 'cap' (a capped count), 'ok' (a page size, a limit, `first` or a
|
|
1347
|
+
* `*_SKELETON_*` name; a caller's count passed through; the length of a list), else 'unnamed': a count that names no
|
|
1348
|
+
* source (a call, `rows.length + 2`, a read's `data?.total`). Locals resolve to their initializer, and a field of a local
|
|
1349
|
+
* object (`settings.rows`) to its value there.
|
|
1350
|
+
*/
|
|
1351
|
+
function classifyCount(expr, model, { attr = '', depth = 0 } = {}) {
|
|
1352
|
+
const n = strip(expr);
|
|
1353
|
+
if (!n || depth > 5) return 'ok';
|
|
1354
|
+
const next = e => classifyCount(e, model, { attr, depth: depth + 1 });
|
|
1355
|
+
if (ts.isNumericLiteral(n)) return 'typed';
|
|
1356
|
+
// Not a number at all: a list of row shapes (`rows={[{ width }]}`), a flag (`count={n !== undefined}`), nothing.
|
|
1357
|
+
if (!isNumberShaped(n)) return 'ok';
|
|
1358
|
+
if (
|
|
1359
|
+
ts.isBinaryExpression(n) &&
|
|
1360
|
+
(n.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || n.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
1361
|
+
)
|
|
1362
|
+
return ts.isNumericLiteral(strip(n.right))
|
|
1363
|
+
? strip(n.right).text === '0'
|
|
1364
|
+
? next(n.left)
|
|
1365
|
+
: 'typed'
|
|
1366
|
+
: worst([n.left, n.right].filter(b => !isNullish(b)).map(next));
|
|
1367
|
+
// Arithmetic on a count: a named count or a caller's count scaled or split (`PAGE_SIZE * 2`, `rows - 1`) is that
|
|
1368
|
+
// count; anything else is unnamed (`rows.length + 2` draws two typed rows while the list is empty).
|
|
1369
|
+
if (ts.isBinaryExpression(n) && ARITHMETIC.has(n.operatorToken.kind)) {
|
|
1370
|
+
const operands = [n.left, n.right].map(strip);
|
|
1371
|
+
const counts = operands.filter(o => !ts.isNumericLiteral(o));
|
|
1372
|
+
if (counts.length && counts.every(o => namesCount(o, model) && next(o) === 'ok')) return 'ok';
|
|
1373
|
+
// What is left of a named count (`pageSize - items.length`: the rest of the page) is that count.
|
|
1374
|
+
if (n.operatorToken.kind === ts.SyntaxKind.MinusToken && namesCount(n.left, model) && next(n.left) === 'ok')
|
|
1375
|
+
return 'ok';
|
|
1376
|
+
return worst([...counts.map(next), 'unnamed']);
|
|
1377
|
+
}
|
|
1378
|
+
if (ts.isConditionalExpression(n)) {
|
|
1379
|
+
// A cap: `PAGE_SIZE > 6 ? 6 : PAGE_SIZE`, or a literal against a page size. Two literals (`isMobile ? 3 : 10`)
|
|
1380
|
+
// are two typed counts.
|
|
1381
|
+
const branches = [n.whenTrue, n.whenFalse].map(strip);
|
|
1382
|
+
if (isComparison(n.condition)) return 'cap';
|
|
1383
|
+
if (branches.some(b => ts.isNumericLiteral(b)) && branches.some(b => COUNT_SOURCE_NAME.test(leafName(b))))
|
|
1384
|
+
return 'cap';
|
|
1385
|
+
// Two literals choose between two forms, unless the choice is the screen's width or the count a skeleton count.
|
|
1386
|
+
if (branches.every(b => ts.isNumericLiteral(b)) && !attr && !WIDTH_TEST.test(n.condition.getText())) return 'ok';
|
|
1387
|
+
return worst([n.whenTrue, n.whenFalse].map(next));
|
|
1388
|
+
}
|
|
1389
|
+
if (ts.isCallExpression(n)) {
|
|
1390
|
+
const callee = dottedName(n.expression);
|
|
1391
|
+
if (callee === 'Math.min' && isPageCap(n, model)) return 'cap';
|
|
1392
|
+
// `Math.min(PAGE_SIZE, rows.length)` (the last page), a clamp: named when one of its arguments is.
|
|
1393
|
+
if (callee === 'Math.min' || callee === 'Math.max')
|
|
1394
|
+
return n.arguments.some(a => next(a) === 'ok') ? 'ok' : 'unnamed';
|
|
1395
|
+
return COUNT_SOURCE_NAME.test(callee.split('.').pop() ?? '') ? 'ok' : 'unnamed';
|
|
1396
|
+
}
|
|
1397
|
+
if (ts.isPropertyAccessExpression(n) && n.name.text === 'length') {
|
|
1398
|
+
const of = strip(n.expression);
|
|
1399
|
+
if (!ts.isIdentifier(of) || COUNT_SOURCE_NAME.test(of.text)) return 'ok';
|
|
1400
|
+
const ofDecl = model.lookup(of.text, of);
|
|
1401
|
+
return ofDecl?.kind === 'var' && ofDecl.init && isLiteralArray(ofDecl.init) ? 'unnamed' : 'ok';
|
|
1402
|
+
}
|
|
1403
|
+
if (ts.isIdentifier(n) || ts.isPropertyAccessExpression(n)) {
|
|
1404
|
+
const name = leafName(n);
|
|
1405
|
+
if (CAP_NAME.test(name)) return 'cap';
|
|
1406
|
+
const root = chainRoot(n);
|
|
1407
|
+
const decl = root ? model.lookup(root.text, root) : null;
|
|
1408
|
+
// A count handed in by the caller (a prop, or a field of one) is a pass-through: checked where the caller names it.
|
|
1409
|
+
if (decl?.kind === 'param') return 'ok';
|
|
1410
|
+
// Destructured from the props (`const { rows = X } = props`): a pass-through.
|
|
1411
|
+
if (decl?.kind === 'var' && decl.destructured && decl.init && ts.isIdentifier(strip(decl.init))) {
|
|
1412
|
+
const from = model.lookup(strip(decl.init).text, decl.init);
|
|
1413
|
+
if (from?.kind === 'param') return 'ok';
|
|
1414
|
+
}
|
|
1415
|
+
const local = decl?.kind === 'var' && !!decl.init && !decl.destructured;
|
|
1416
|
+
if (COUNT_SOURCE_NAME.test(name)) {
|
|
1417
|
+
// A declared page size, limit or `*_SKELETON_*` name is the source, whatever it is set from, unless it caps.
|
|
1418
|
+
return local && ts.isIdentifier(n) && next(decl.init) === 'cap' ? 'cap' : 'ok';
|
|
1419
|
+
}
|
|
1420
|
+
// The same name as the count it fills (`skeletonRows={skeletonRows}`), not set here: a pass-through.
|
|
1421
|
+
if (attr && name === attr && !local) return 'ok';
|
|
1422
|
+
if (local) {
|
|
1423
|
+
if (ts.isIdentifier(n)) return next(decl.init);
|
|
1424
|
+
// A field of a local object (`const settings = { rows: 4 }` -> `settings.rows`) is its value there.
|
|
1425
|
+
const value = objectField(decl.init, propertyPath(n));
|
|
1426
|
+
if (value) return next(value);
|
|
1427
|
+
}
|
|
1428
|
+
// A skeleton-count field of something read (`scroll.skeletonRows`): checked where its key is set.
|
|
1429
|
+
if (ts.isPropertyAccessExpression(n) && COUNT_ATTR.test(name)) return 'ok';
|
|
1430
|
+
if (decl?.kind === 'import') return COUNT_SOURCE_NAME.test(decl.imported) ? 'ok' : 'unnamed';
|
|
1431
|
+
return 'unnamed';
|
|
1432
|
+
}
|
|
1433
|
+
return 'unnamed';
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/** Whether an expression can be a number (not a literal list, object, string, boolean, comparison or `!x`). */
|
|
1437
|
+
function isNumberShaped(node) {
|
|
1438
|
+
if (
|
|
1439
|
+
ts.isArrayLiteralExpression(node) ||
|
|
1440
|
+
ts.isObjectLiteralExpression(node) ||
|
|
1441
|
+
ts.isStringLiteralLike(node) ||
|
|
1442
|
+
ts.isTemplateExpression(node) ||
|
|
1443
|
+
ts.isTypeOfExpression(node) ||
|
|
1444
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1445
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1446
|
+
(ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken)
|
|
1447
|
+
)
|
|
1448
|
+
return false;
|
|
1449
|
+
return !(ts.isBinaryExpression(node) && (isComparison(node) || EQUALITY.has(node.operatorToken.kind)));
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Whether an operand of count arithmetic is a count itself: a caller's (a prop) or one named as a source. */
|
|
1453
|
+
function namesCount(node, model) {
|
|
1454
|
+
const n = strip(node);
|
|
1455
|
+
if (ts.isBinaryExpression(n) && ARITHMETIC.has(n.operatorToken.kind)) return true;
|
|
1456
|
+
if (!ts.isIdentifier(n) && !ts.isPropertyAccessExpression(n)) return false;
|
|
1457
|
+
// The length of a list (`rows.length`) is what is read, empty while loading: not a count to scale.
|
|
1458
|
+
if (leafName(n) === 'length') return false;
|
|
1459
|
+
if (COUNT_SOURCE_NAME.test(leafName(n))) return true;
|
|
1460
|
+
const root = chainRoot(n);
|
|
1461
|
+
return !!root && model.lookup(root.text, root)?.kind === 'param';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** The field names of a property access below its root (`a.b.c` -> `['b', 'c']`). */
|
|
1465
|
+
function propertyPath(node) {
|
|
1466
|
+
const path = [];
|
|
1467
|
+
let n = strip(node);
|
|
1468
|
+
while (n && ts.isPropertyAccessExpression(n)) {
|
|
1469
|
+
path.unshift(n.name.text);
|
|
1470
|
+
n = strip(n.expression);
|
|
1471
|
+
}
|
|
1472
|
+
return path;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** The value of `path` in an object literal (`{ a: { b: 4 } }`, `['a', 'b']` -> `4`), or null. */
|
|
1476
|
+
function objectField(init, path) {
|
|
1477
|
+
let value = strip(init);
|
|
1478
|
+
for (const key of path) {
|
|
1479
|
+
if (!value || !ts.isObjectLiteralExpression(value)) return null;
|
|
1480
|
+
const prop = value.properties.find(
|
|
1481
|
+
q =>
|
|
1482
|
+
(ts.isPropertyAssignment(q) || ts.isShorthandPropertyAssignment(q)) &&
|
|
1483
|
+
q.name.getText().replace(/['"]/g, '') === key,
|
|
1484
|
+
);
|
|
1485
|
+
if (!prop) return null;
|
|
1486
|
+
value = strip(ts.isPropertyAssignment(prop) ? prop.initializer : prop.name);
|
|
1487
|
+
}
|
|
1488
|
+
return value;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function isLiteralArray(init) {
|
|
1492
|
+
const n = strip(init);
|
|
1493
|
+
return (
|
|
1494
|
+
!!n && ts.isArrayLiteralExpression(n) && n.elements.every(e => ts.isNumericLiteral(e) || ts.isStringLiteral(e))
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function isComparison(node) {
|
|
1499
|
+
const n = strip(node);
|
|
1500
|
+
return (
|
|
1501
|
+
!!n &&
|
|
1502
|
+
ts.isBinaryExpression(n) &&
|
|
1503
|
+
[
|
|
1504
|
+
ts.SyntaxKind.GreaterThanToken,
|
|
1505
|
+
ts.SyntaxKind.GreaterThanEqualsToken,
|
|
1506
|
+
ts.SyntaxKind.LessThanToken,
|
|
1507
|
+
ts.SyntaxKind.LessThanEqualsToken,
|
|
1508
|
+
].includes(n.operatorToken.kind)
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** `Math.min(MAX, Math.max(MIN, x))` / `Math.max(MIN, Math.min(MAX, x))`: a clamp of a value, not a capped count. */
|
|
1513
|
+
function isClamp(call) {
|
|
1514
|
+
const inner = call.arguments.some(
|
|
1515
|
+
a => ts.isCallExpression(strip(a)) && dottedName(strip(a).expression) === 'Math.max',
|
|
1516
|
+
);
|
|
1517
|
+
const outer = ts.isCallExpression(call.parent) && dottedName(call.parent.expression) === 'Math.max';
|
|
1518
|
+
return inner || outer;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* `Math.min(pageSize, X)` where X is a fold, a constant, a number or a local holding one (`rowsShown = 6`); not the last
|
|
1523
|
+
* page (`rows.length`, `total`).
|
|
1524
|
+
*/
|
|
1525
|
+
function isPageCap(call, model) {
|
|
1526
|
+
const args = call.arguments.map(a => a.getText().trim());
|
|
1527
|
+
const page = args.find(a => PAGE_ARG.test(a));
|
|
1528
|
+
if (!page) return false;
|
|
1529
|
+
return call.arguments.some((arg, i) => {
|
|
1530
|
+
const text = args[i];
|
|
1531
|
+
if (text === page) return false;
|
|
1532
|
+
if ((/^[\w$.]+$/.test(text) || CAP_NAME.test(text)) && isCapArg(text)) return true;
|
|
1533
|
+
const n = strip(arg);
|
|
1534
|
+
if (!ts.isIdentifier(n)) return false;
|
|
1535
|
+
if (CAP_NAME.test(n.text)) return true;
|
|
1536
|
+
const decl = model.lookup(n.text, n);
|
|
1537
|
+
return decl?.kind === 'var' && !decl.destructured && !!decl.init && ts.isNumericLiteral(strip(decl.init));
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/** The length expression and item callback of a list drawn N times, or null. */
|
|
1542
|
+
function listOf(node, model) {
|
|
1543
|
+
// Array.from({ length: E }, cb?)
|
|
1544
|
+
if (ts.isCallExpression(node) && dottedName(node.expression) === 'Array.from') {
|
|
1545
|
+
const first = strip(node.arguments[0]);
|
|
1546
|
+
if (first && ts.isObjectLiteralExpression(first)) {
|
|
1547
|
+
const length = first.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText() === 'length');
|
|
1548
|
+
if (length) return { count: length.initializer, callback: node.arguments[1] ?? mappedCallback(node), node };
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
// Array(E) / new Array(E) (mapped, spread or filled)
|
|
1553
|
+
if (
|
|
1554
|
+
(ts.isCallExpression(node) || ts.isNewExpression(node)) &&
|
|
1555
|
+
dottedName(node.expression) === 'Array' &&
|
|
1556
|
+
node.arguments?.length === 1
|
|
1557
|
+
) {
|
|
1558
|
+
return { count: node.arguments[0], callback: mappedCallback(spreadHost(node)), node };
|
|
1559
|
+
}
|
|
1560
|
+
// [0, 1, 2].map(cb)
|
|
1561
|
+
if (
|
|
1562
|
+
ts.isArrayLiteralExpression(node) &&
|
|
1563
|
+
isIndexList(node) &&
|
|
1564
|
+
ts.isPropertyAccessExpression(node.parent) &&
|
|
1565
|
+
node.parent.name.text === 'map'
|
|
1566
|
+
) {
|
|
1567
|
+
return { count: node, callback: mappedCallback(node), node, literalArray: true };
|
|
1568
|
+
}
|
|
1569
|
+
// <LoadingItems count={E}>{cb}</LoadingItems>
|
|
1570
|
+
if (
|
|
1571
|
+
(ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) &&
|
|
1572
|
+
model.resolveName(openingOf(node).tagName) === 'LoadingItems'
|
|
1573
|
+
) {
|
|
1574
|
+
const count = attributeExpression(attribute(openingOf(node), 'count'));
|
|
1575
|
+
const child = ts.isJsxElement(node)
|
|
1576
|
+
? node.children.find(c => ts.isJsxExpression(c) && c.expression)?.expression
|
|
1577
|
+
: attributeExpression(attribute(node, 'children'));
|
|
1578
|
+
return { count, callback: child, node, loadingItems: true };
|
|
1579
|
+
}
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/** `[0, 1, 2]` / `[1, 2, 3, 4]`: a list of indexes standing for N items (not a table of widths or line counts). */
|
|
1584
|
+
function isIndexList(node) {
|
|
1585
|
+
const first = node.elements[0];
|
|
1586
|
+
const base = first && ts.isNumericLiteral(first) ? Number(first.text) : NaN;
|
|
1587
|
+
return (
|
|
1588
|
+
node.elements.length > 1 &&
|
|
1589
|
+
(base === 0 || base === 1) &&
|
|
1590
|
+
node.elements.every((e, i) => ts.isNumericLiteral(e) && Number(e.text) === base + i)
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/** `[...Array(n)]` -> the array literal (whose `.map` draws the items). */
|
|
1595
|
+
function spreadHost(node) {
|
|
1596
|
+
let n = node;
|
|
1597
|
+
if (n.parent && ts.isSpreadElement(n.parent) && ts.isArrayLiteralExpression(n.parent.parent)) n = n.parent.parent;
|
|
1598
|
+
// Array(n).fill(x).map / Array(n).keys()
|
|
1599
|
+
while (
|
|
1600
|
+
ts.isPropertyAccessExpression(n.parent) &&
|
|
1601
|
+
['fill', 'keys'].includes(n.parent.name.text) &&
|
|
1602
|
+
ts.isCallExpression(n.parent.parent)
|
|
1603
|
+
)
|
|
1604
|
+
n = n.parent.parent;
|
|
1605
|
+
return n;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function mappedCallback(node) {
|
|
1609
|
+
let n = node;
|
|
1610
|
+
while (n.parent && ts.isParenthesizedExpression(n.parent)) n = n.parent;
|
|
1611
|
+
if (ts.isPropertyAccessExpression(n.parent) && n.parent.name.text === 'map' && ts.isCallExpression(n.parent.parent))
|
|
1612
|
+
return n.parent.parent.arguments[0];
|
|
1613
|
+
return undefined;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function callbackItem(callback) {
|
|
1617
|
+
const cb = strip(callback);
|
|
1618
|
+
if (!cb || !isFunctionLike(cb)) return null;
|
|
1619
|
+
return rootElement(returnedExpression(cb));
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/** Whether an item element is marked (on itself, by `loadingItemProps(…)`, or inside its component). */
|
|
1623
|
+
function itemMarked(el, model) {
|
|
1624
|
+
if (!el) return true; // nothing to read: not a list of elements
|
|
1625
|
+
if (ts.isJsxFragment(el)) {
|
|
1626
|
+
const first = el.children.find(c => ts.isJsxElement(c) || ts.isJsxSelfClosingElement(c));
|
|
1627
|
+
return first ? itemMarked(first, model) : true;
|
|
1628
|
+
}
|
|
1629
|
+
const opening = openingOf(el);
|
|
1630
|
+
for (const attr of opening.attributes.properties) {
|
|
1631
|
+
if (ts.isJsxAttribute(attr) && ITEM_MARK_ATTR.test(attr.name.getText())) return true;
|
|
1632
|
+
if (ts.isJsxSpreadAttribute(attr) && /loadingItemProps\s*\(|data-loading-item/.test(attr.expression.getText()))
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
const tag = dottedName(opening.tagName);
|
|
1636
|
+
if (!isComponentName(tag)) return false;
|
|
1637
|
+
return model.project.componentMarks(model.path, tag.split('.')[0], 'item');
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/** The first `.map(cb)` inside `node` (not inside a nested function), or null. */
|
|
1641
|
+
function firstMap(node) {
|
|
1642
|
+
let found = null;
|
|
1643
|
+
const visit = n => {
|
|
1644
|
+
if (found) return;
|
|
1645
|
+
if (
|
|
1646
|
+
ts.isCallExpression(n) &&
|
|
1647
|
+
ts.isPropertyAccessExpression(n.expression) &&
|
|
1648
|
+
n.expression.name.text === 'map' &&
|
|
1649
|
+
n.arguments[0] &&
|
|
1650
|
+
isFunctionLike(strip(n.arguments[0]))
|
|
1651
|
+
) {
|
|
1652
|
+
found = n;
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (isFunctionLike(n)) return;
|
|
1656
|
+
ts.forEachChild(n, visit);
|
|
1657
|
+
};
|
|
1658
|
+
visit(node);
|
|
1659
|
+
return found;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* The `.map(cb)` of the loaded list beside a loading list: the other branch of the same condition, or the list the
|
|
1664
|
+
* component returns after `if (loading) return …` (or in its `else`).
|
|
1665
|
+
*/
|
|
1666
|
+
function loadedListBeside(listNode, model) {
|
|
1667
|
+
let child = listNode;
|
|
1668
|
+
for (let p = listNode.parent; p && !isFunctionLike(p); child = p, p = p.parent) {
|
|
1669
|
+
if (ts.isConditionalExpression(p) && (isLoadingFlag(p.condition, model) || isNegatedLoading(p.condition, model))) {
|
|
1670
|
+
const other = child === p.whenTrue ? p.whenFalse : child === p.whenFalse ? p.whenTrue : null;
|
|
1671
|
+
return other ? firstMap(other) : null;
|
|
1672
|
+
}
|
|
1673
|
+
if (ts.isIfStatement(p) && child === p.thenStatement && isLoadingFlag(p.expression, model)) {
|
|
1674
|
+
if (p.elseStatement) return firstMap(p.elseStatement);
|
|
1675
|
+
const block = p.parent;
|
|
1676
|
+
if (!ts.isBlock(block) || !containsReturn(p.thenStatement)) return null;
|
|
1677
|
+
for (const st of block.statements.slice(block.statements.indexOf(p) + 1)) {
|
|
1678
|
+
const found = ts.isReturnStatement(st) && st.expression ? firstMap(st.expression) : null;
|
|
1679
|
+
if (found) return found;
|
|
1680
|
+
}
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1688
|
+
// The analysis
|
|
1689
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Every finding over the reported trees of `project`, as `{ rule, path, file, line, message }` (`file` is relative to
|
|
1693
|
+
* the finding's tree), under a config (`CONFIGS.lib`, `CONFIGS.frontend`): `rules` run, `trees` reported, `pageCode`,
|
|
1694
|
+
* `exempt` lifting rules from files, and `files` (absolute paths) narrowing the run.
|
|
1695
|
+
*
|
|
1696
|
+
* @param {Project} project
|
|
1697
|
+
* @param {{ rules?: string[], trees?: string[], pageCode?: Function, exempt?: Array<{ why?: string, files: RegExp, rules: string[] }>, files?: string[] }} [config]
|
|
1698
|
+
*/
|
|
1699
|
+
export function analyzeProject(project, { rules = ALL_RULES, trees, pageCode, exempt = [], files } = {}) {
|
|
1700
|
+
const enabled = new Set(rules);
|
|
1701
|
+
const findings = [];
|
|
1702
|
+
const paths = files ?? [...project.text.keys()].filter(path => project.treeOf.get(path)?.report);
|
|
1703
|
+
for (const path of paths) {
|
|
1704
|
+
const tree = project.treeOf.get(path);
|
|
1705
|
+
const rel = tree ? relative(tree.dir, path) : path;
|
|
1706
|
+
if (trees && !trees.some(dir => rel === dir || rel.startsWith(`${dir}/`))) continue;
|
|
1707
|
+
const lifted = new Set(exempt.filter(e => e.files.test(rel)).flatMap(e => e.rules));
|
|
1708
|
+
const active = new Set([...enabled].filter(r => !lifted.has(r)));
|
|
1709
|
+
if (!active.size) continue;
|
|
1710
|
+
const isFile = other => project.text.has(join(tree?.dir ?? '', other)) || existsSync(join(tree?.dir ?? '', other));
|
|
1711
|
+
findings.push(...analyzeFile(project, path, active, rel, pageCode?.(rel, isFile) ?? null));
|
|
1712
|
+
}
|
|
1713
|
+
return findings;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/** Every finding in one file (see `analyzeProject`); `page` is the file's page-code setting, or null. */
|
|
1717
|
+
function analyzeFile(project, path, active, rel, page) {
|
|
1718
|
+
const model = project.model(path);
|
|
1719
|
+
const findings = [];
|
|
1720
|
+
const seen = new Set();
|
|
1721
|
+
const report = (rule, node, detail) => {
|
|
1722
|
+
if (!active.has(rule)) return;
|
|
1723
|
+
if (model.optedOut(node, rule)) return;
|
|
1724
|
+
const line = model.line(node);
|
|
1725
|
+
const key = `${rule}:${line}:${detail ?? ''}`;
|
|
1726
|
+
if (seen.has(key)) return;
|
|
1727
|
+
seen.add(key);
|
|
1728
|
+
findings.push({ rule, path, file: rel, line, message: detail ? `${detail}` : RULES[rule] });
|
|
1729
|
+
};
|
|
1730
|
+
const isTsx = path.endsWith('.tsx');
|
|
1731
|
+
const oneSwitchSites = new Set();
|
|
1732
|
+
|
|
1733
|
+
// Registry slots and next/dynamic fallbacks (allowed forms).
|
|
1734
|
+
const exemptRender = (node, component) => {
|
|
1735
|
+
if (inDynamicLoading(node)) return true;
|
|
1736
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
1737
|
+
if (!ts.isPropertyAssignment(p) || p.name.getText() !== 'skeleton' || !ts.isObjectLiteralExpression(p.parent))
|
|
1738
|
+
continue;
|
|
1739
|
+
const render = p.parent.properties.find(q => ts.isPropertyAssignment(q) && q.name.getText() === 'render');
|
|
1740
|
+
if (!render) continue;
|
|
1741
|
+
const renders = new Set();
|
|
1742
|
+
walk(render.initializer, n => {
|
|
1743
|
+
const tag = renderedTag(n);
|
|
1744
|
+
if (tag) renders.add(model.resolveName(tag).replace('Chat', ''));
|
|
1745
|
+
});
|
|
1746
|
+
if (renders.has(component)) return true;
|
|
1747
|
+
}
|
|
1748
|
+
return false;
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
/** Report a parent drawing the loading state of `component` (ONE_SWITCH when it switches itself). */
|
|
1752
|
+
const parentPicks = (node, component, what) => {
|
|
1753
|
+
oneSwitchSites.add(node);
|
|
1754
|
+
if (project.componentSwitches(component))
|
|
1755
|
+
report(
|
|
1756
|
+
'ONE_SWITCH',
|
|
1757
|
+
node,
|
|
1758
|
+
`${what} outside ${component}'s module, which switches itself (render <${component} />: one switch per component)`,
|
|
1759
|
+
);
|
|
1760
|
+
else
|
|
1761
|
+
report(
|
|
1762
|
+
'NO_SCOPE_SWITCH',
|
|
1763
|
+
node,
|
|
1764
|
+
`${what} outside ${component}'s module, which never switches itself (${component} should draw its own skeleton on useLoadingScope(), and this place render <${component} />)`,
|
|
1765
|
+
);
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* A render of a component skeleton (`<YSkeleton>`, `createElement(YSkeleton)`, `{YSkeleton}` as a prop), or of a
|
|
1770
|
+
* loading component not named `*Skeleton` (`<XLoading />`, `<XPlaceholder />`).
|
|
1771
|
+
*/
|
|
1772
|
+
const skeletonRender = (node, nameNode) => {
|
|
1773
|
+
const name = model.resolveName(nameNode);
|
|
1774
|
+
if (WHOLE_VIEW.test(name) && !project.deprecated.has(name)) {
|
|
1775
|
+
if (!project.declaredIn.get(name)?.has(path))
|
|
1776
|
+
report('WHOLE_VIEW_SKELETON', node, `<${name}> (a whole-view skeleton) rendered`);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (
|
|
1780
|
+
(LOADING_COMPONENT.test(name) && !LOADING_WRAPPER.test(name)) ||
|
|
1781
|
+
(PLACEHOLDER_COMPONENT.test(name) && inLoadingContext(node, model))
|
|
1782
|
+
) {
|
|
1783
|
+
const declared = project.declaredIn.get(name);
|
|
1784
|
+
if (declared?.size && !declared.has(path) && !exemptRender(node, name)) {
|
|
1785
|
+
oneSwitchSites.add(node);
|
|
1786
|
+
report(
|
|
1787
|
+
'NO_SCOPE_SWITCH',
|
|
1788
|
+
node,
|
|
1789
|
+
`<${name}> picked as a loading state (the component it stands for should draw its own skeleton on useLoadingScope(), and this place render it)`,
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (!isComponentSkeletonName(name)) return;
|
|
1795
|
+
const component = name.slice(0, -'Skeleton'.length);
|
|
1796
|
+
// Deprecated lib skeletons.
|
|
1797
|
+
if (project.deprecated.has(name) && !project.declaredIn.get(name)?.has(path))
|
|
1798
|
+
report(
|
|
1799
|
+
'DEPRECATED_SKELETON',
|
|
1800
|
+
node,
|
|
1801
|
+
`<${name}> is deprecated in the lib (render the component in a loading scope)`,
|
|
1802
|
+
);
|
|
1803
|
+
if (name === 'DetailPageSkeleton')
|
|
1804
|
+
report('WHOLE_VIEW_SKELETON', node, 'DetailPageSkeleton (a whole-page skeleton)');
|
|
1805
|
+
if (PRIMITIVE.test(name)) return;
|
|
1806
|
+
const modules = project.declaredIn.get(component);
|
|
1807
|
+
const skeletonFn = enclosingNames(node).find(isSkeletonFunctionName);
|
|
1808
|
+
if (!modules?.size) {
|
|
1809
|
+
// A skeleton with no component of its own, chosen by a parent: a loading state drawn by the parent.
|
|
1810
|
+
const own = project.declaredIn.get(name);
|
|
1811
|
+
// A part of a component family (declared in the same folder) is the family's own.
|
|
1812
|
+
const family = [...(own ?? [])].some(d => dirname(d) === dirname(path));
|
|
1813
|
+
if (!skeletonFn && own?.size && !family && !project.deprecated.has(name) && !exemptRender(node, component)) {
|
|
1814
|
+
oneSwitchSites.add(node);
|
|
1815
|
+
report(
|
|
1816
|
+
'NO_SCOPE_SWITCH',
|
|
1817
|
+
node,
|
|
1818
|
+
`<${name}> picked as a loading state with no component of its own (draw the component the skeleton stands for, in a loading scope)`,
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const own = new Set([...modules, ...(project.declaredIn.get(name) ?? [])]);
|
|
1824
|
+
if (own.has(path)) return;
|
|
1825
|
+
if (exemptRender(node, component)) return;
|
|
1826
|
+
if (skeletonFn) {
|
|
1827
|
+
if (rendersOutsideSkeletons(model, component) && project.componentSwitches(component)) {
|
|
1828
|
+
oneSwitchSites.add(node);
|
|
1829
|
+
report(
|
|
1830
|
+
'ONE_SWITCH',
|
|
1831
|
+
node,
|
|
1832
|
+
`<${name}> rendered by ${skeletonFn}, while this module renders <${component} />, which switches itself (render <${component} /> in ${skeletonFn} too: one switch per component)`,
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
parentPicks(node, component, `<${name}> rendered`);
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
const components = [];
|
|
1841
|
+
|
|
1842
|
+
const visit = node => {
|
|
1843
|
+
// --- declarations -------------------------------------------------------------------------------------------
|
|
1844
|
+
if (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) {
|
|
1845
|
+
const name = node.name && ts.isIdentifier(node.name) ? node.name.text : '';
|
|
1846
|
+
const isDeprecated = ts
|
|
1847
|
+
.getJSDocTags(ts.isVariableDeclaration(node) ? node.parent.parent : node)
|
|
1848
|
+
.some(t => t.tagName.text === 'deprecated');
|
|
1849
|
+
const fnLike =
|
|
1850
|
+
ts.isFunctionDeclaration(node) ||
|
|
1851
|
+
(node.initializer && (functionInside(node.initializer) || isJsx(node.initializer)));
|
|
1852
|
+
if (name && WHOLE_VIEW.test(name) && fnLike && !isDeprecated)
|
|
1853
|
+
report('WHOLE_VIEW_SKELETON', node.name, `${name} (a whole-view skeleton)`);
|
|
1854
|
+
}
|
|
1855
|
+
if (isFunctionLike(node)) {
|
|
1856
|
+
const name = functionName(node);
|
|
1857
|
+
if (isComponentName(name) && !isSkeletonFunctionName(name)) components.push(node);
|
|
1858
|
+
// Literal count defaults: `skeletonRows = 8` anywhere, `rows = 6` / `count = 6` / `steps = 6` in a `*Skeleton`.
|
|
1859
|
+
checkCountDefaults(node, isSkeletonFunctionName(name));
|
|
1860
|
+
}
|
|
1861
|
+
if (
|
|
1862
|
+
ts.isVariableDeclaration(node) &&
|
|
1863
|
+
ts.isIdentifier(node.name) &&
|
|
1864
|
+
/^(?:skeletonRows|skeletonCount)$/.test(node.name.text) &&
|
|
1865
|
+
node.initializer
|
|
1866
|
+
) {
|
|
1867
|
+
if (classifyCount(node.initializer, model) === 'typed' && containsLiteralFallback(node.initializer))
|
|
1868
|
+
report('TYPED_COUNT', node, `a literal fallback for ${node.name.text} (name the page size)`);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// --- identifiers: retired APIs ---------------------------------------------------------------------------------
|
|
1872
|
+
if (ts.isIdentifier(node) && isRetiredName(node.text)) {
|
|
1873
|
+
if (!(
|
|
1874
|
+
ts.isImportSpecifier(node.parent) ||
|
|
1875
|
+
(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
|
|
1876
|
+
))
|
|
1877
|
+
report(
|
|
1878
|
+
'RETIRED_API',
|
|
1879
|
+
node,
|
|
1880
|
+
MEMORY.has(node.text)
|
|
1881
|
+
? `${node.text} (the removed first-paint memory)`
|
|
1882
|
+
: `${node.text} (stand-in data is retired: draw the component's skeleton)`,
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// --- strings: pulse, spinner, loading text, hidden items -------------------------------------------------------
|
|
1887
|
+
if (
|
|
1888
|
+
ts.isStringLiteral(node) ||
|
|
1889
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
1890
|
+
ts.isTemplateHead(node) ||
|
|
1891
|
+
ts.isTemplateMiddle(node) ||
|
|
1892
|
+
ts.isTemplateTail(node)
|
|
1893
|
+
) {
|
|
1894
|
+
checkString(node, node.text);
|
|
1895
|
+
}
|
|
1896
|
+
if (isTsx && ts.isJsxText(node)) {
|
|
1897
|
+
const text = node.text.trim();
|
|
1898
|
+
if ((WAIT_TEXT.test(text) || WAIT_ELLIPSIS.test(text)) && !inAnnouncement(node))
|
|
1899
|
+
report('LOADING_TEXT', node, `"${text}" as a loading state (draw the component skeleton)`);
|
|
1900
|
+
}
|
|
1901
|
+
// An inline animation of one's own (`style={{ animation: 'pulse 2s infinite' }}`).
|
|
1902
|
+
if (
|
|
1903
|
+
ts.isPropertyAssignment(node) &&
|
|
1904
|
+
/^(?:animation|animationName)$/.test(node.name.getText().replace(/['"]/g, '')) &&
|
|
1905
|
+
stringsIn(node.initializer).some(s => /\b(?:pulse|spin)\b/.test(s))
|
|
1906
|
+
)
|
|
1907
|
+
report('OWN_PULSE', node, 'an inline pulse or spin animation of its own (use the lib skeletons)');
|
|
1908
|
+
// A count typed in an object (`{ skeletonRows: 5, loadingRows: { primary: 7 } }`).
|
|
1909
|
+
if (ts.isPropertyAssignment(node) && COUNT_ATTR.test(node.name.getText().replace(/['"]/g, ''))) {
|
|
1910
|
+
const value = strip(node.initializer);
|
|
1911
|
+
const values = ts.isObjectLiteralExpression(value)
|
|
1912
|
+
? value.properties.filter(ts.isPropertyAssignment).map(q => q.initializer)
|
|
1913
|
+
: [value];
|
|
1914
|
+
for (const v of values) {
|
|
1915
|
+
const kind = classifyCount(v, model, { attr: node.name.getText() });
|
|
1916
|
+
if (kind === 'typed')
|
|
1917
|
+
report(
|
|
1918
|
+
'TYPED_COUNT',
|
|
1919
|
+
v,
|
|
1920
|
+
`a typed skeleton count (${node.name.getText()}: ${v.getText()}); name the list's page size, limit or a *_SKELETON_* constant`,
|
|
1921
|
+
);
|
|
1922
|
+
else if (kind === 'cap')
|
|
1923
|
+
report(
|
|
1924
|
+
'COUNT_CAP',
|
|
1925
|
+
v,
|
|
1926
|
+
`a capped skeleton count (${node.name.getText()}: ${v.getText()}); a list's skeleton draws its full page`,
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// --- JSX elements ------------------------------------------------------------------------------------------------
|
|
1932
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) checkElement(node);
|
|
1933
|
+
if (ts.isJsxAttribute(node)) checkAttribute(node);
|
|
1934
|
+
|
|
1935
|
+
// --- calls ---------------------------------------------------------------------------------------------------------
|
|
1936
|
+
if (ts.isCallExpression(node)) {
|
|
1937
|
+
const callee = dottedName(node.expression);
|
|
1938
|
+
const tag = renderedTag(node);
|
|
1939
|
+
if (tag) {
|
|
1940
|
+
skeletonRender(node, tag);
|
|
1941
|
+
if (LOADERS.has(model.resolveName(tag))) report('LOADER_COMPONENT', node, RULES.LOADER_COMPONENT);
|
|
1942
|
+
}
|
|
1943
|
+
if (isDynamicCall(node)) checkDynamic(node);
|
|
1944
|
+
if (callee === 'Math.min' && isPageCap(node, model) && !isClamp(node))
|
|
1945
|
+
report(
|
|
1946
|
+
'COUNT_CAP',
|
|
1947
|
+
node,
|
|
1948
|
+
`a page size or limit capped (\`${node.getText()}\`); a list's skeleton draws its full page`,
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// --- lists drawn N times ---------------------------------------------------------------------------------------------
|
|
1953
|
+
const list = listOf(node, model);
|
|
1954
|
+
if (list) checkList(list);
|
|
1955
|
+
|
|
1956
|
+
// --- module constants that are literal lists, used while loading -------------------------------------------------
|
|
1957
|
+
if (ts.isIdentifier(node) && !ts.isVariableDeclaration(node.parent) && inLoadingContext(node, model)) {
|
|
1958
|
+
const decl = model.lookup(node.text, node);
|
|
1959
|
+
if (
|
|
1960
|
+
decl?.kind === 'var' &&
|
|
1961
|
+
decl.init &&
|
|
1962
|
+
ts.isSourceFile(decl.node.parent.parent.parent) &&
|
|
1963
|
+
isLiteralList(decl.init)
|
|
1964
|
+
)
|
|
1965
|
+
report(
|
|
1966
|
+
'TYPED_COUNT',
|
|
1967
|
+
node,
|
|
1968
|
+
`${node.text} is a literal-length list used while loading; name a page size, limit or *_SKELETON_* count`,
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// --- nothing while loading ----------------------------------------------------------------------------------------
|
|
1973
|
+
if (ts.isIfStatement(node)) checkIfNull(node);
|
|
1974
|
+
if (ts.isConditionalExpression(node)) checkTernaryNull(node);
|
|
1975
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
1976
|
+
checkAndPart(node);
|
|
1977
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.BarBarToken) checkOrPart(node);
|
|
1978
|
+
|
|
1979
|
+
ts.forEachChild(node, visit);
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* A literal default on a count: `skeletonRows = 8` / `skeletonCount = 8` anywhere, `rows` / `count` / `steps` in a
|
|
1984
|
+
* `*Skeleton`, and ANY parameter (a prop, or a field destructured from the props) whose value reaches a skeleton count
|
|
1985
|
+
* in the function (`LoadingItems`' `count`, a skeleton count prop, the length of a list drawn while loading, a
|
|
1986
|
+
* `skeletonCount:` key).
|
|
1987
|
+
*/
|
|
1988
|
+
function checkCountDefaults(fn, inSkeleton) {
|
|
1989
|
+
const defaults = [];
|
|
1990
|
+
const collect = binding => {
|
|
1991
|
+
if (ts.isObjectBindingPattern(binding)) {
|
|
1992
|
+
for (const el of binding.elements) {
|
|
1993
|
+
if (el.initializer && ts.isIdentifier(el.name))
|
|
1994
|
+
defaults.push({ key: (el.propertyName ?? el.name).getText(), name: el.name.text, el });
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
for (const param of fn.parameters) {
|
|
1999
|
+
if (ts.isIdentifier(param.name) && param.initializer)
|
|
2000
|
+
defaults.push({ key: param.name.text, name: param.name.text, el: param });
|
|
2001
|
+
collect(param.name);
|
|
2002
|
+
}
|
|
2003
|
+
// `const { rows = 6 } = props`: a field of the props.
|
|
2004
|
+
const body = bodyOf(fn);
|
|
2005
|
+
for (const st of body && ts.isBlock(body) ? body.statements : []) {
|
|
2006
|
+
if (!ts.isVariableStatement(st)) continue;
|
|
2007
|
+
for (const d of st.declarationList.declarations) {
|
|
2008
|
+
const init = d.initializer && strip(d.initializer);
|
|
2009
|
+
if (init && ts.isIdentifier(init) && model.lookup(init.text, init)?.kind === 'param') collect(d.name);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// A literal other than 0 (a default of none draws nothing).
|
|
2013
|
+
const literals = defaults.filter(d => {
|
|
2014
|
+
const init = d.el.initializer && strip(d.el.initializer);
|
|
2015
|
+
return !!init && ts.isNumericLiteral(init) && init.text !== '0';
|
|
2016
|
+
});
|
|
2017
|
+
if (!literals.length) return;
|
|
2018
|
+
const reached = countNamesIn(fn);
|
|
2019
|
+
for (const { key, name, el } of literals) {
|
|
2020
|
+
if (
|
|
2021
|
+
/^(?:skeletonRows|skeletonCount)$/.test(key) ||
|
|
2022
|
+
(inSkeleton && /^(?:count|rows|steps)$/.test(key)) ||
|
|
2023
|
+
reached.has(name)
|
|
2024
|
+
)
|
|
2025
|
+
report(
|
|
2026
|
+
'TYPED_COUNT',
|
|
2027
|
+
el,
|
|
2028
|
+
`a literal default for ${key}, which a skeleton count draws (name the page size, limit or a *_SKELETON_* constant)`,
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** The names (parameters and props of `fn`, through its locals) that a skeleton count in `fn` reads. */
|
|
2034
|
+
function countNamesIn(fn) {
|
|
2035
|
+
const names = new Set();
|
|
2036
|
+
const read = (expr, depth = 0) => {
|
|
2037
|
+
const visitName = n => {
|
|
2038
|
+
if (!ts.isIdentifier(n)) return undefined;
|
|
2039
|
+
const decl = model.lookup(n.text, n);
|
|
2040
|
+
if (decl?.kind === 'param') names.add(n.text);
|
|
2041
|
+
else if (decl?.kind === 'var' && decl.init && depth < 3 && nearestFunction(decl.node) === fn) {
|
|
2042
|
+
names.add(n.text);
|
|
2043
|
+
if (!decl.destructured) read(decl.init, depth + 1);
|
|
2044
|
+
}
|
|
2045
|
+
return undefined;
|
|
2046
|
+
};
|
|
2047
|
+
visitName(strip(expr));
|
|
2048
|
+
walk(expr, visitName);
|
|
2049
|
+
};
|
|
2050
|
+
walk(fn, n => {
|
|
2051
|
+
if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
|
|
2052
|
+
const tag = model.resolveName(n.tagName);
|
|
2053
|
+
for (const a of n.attributes.properties) {
|
|
2054
|
+
if (ts.isJsxAttribute(a) && attributeExpression(a) && isCountAttribute(tag, a.name.getText()))
|
|
2055
|
+
read(attributeExpression(a));
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
if (ts.isPropertyAssignment(n) && COUNT_ATTR.test(n.name.getText().replace(/['"]/g, ''))) read(n.initializer);
|
|
2059
|
+
const list = listOf(n, model);
|
|
2060
|
+
if (list?.count && !list.literalArray) {
|
|
2061
|
+
const countName = leafName(list.count);
|
|
2062
|
+
// The cells of one row and a text's lines are not list items.
|
|
2063
|
+
if (!isCellCount(countName) && !isTextLineCount(countName) && inLoadingContext(n, model)) read(list.count);
|
|
2064
|
+
}
|
|
2065
|
+
return undefined;
|
|
2066
|
+
});
|
|
2067
|
+
return names;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function containsLiteralFallback(expr) {
|
|
2071
|
+
let hit = false;
|
|
2072
|
+
const v = n => {
|
|
2073
|
+
if (hit) return;
|
|
2074
|
+
if (
|
|
2075
|
+
ts.isBinaryExpression(n) &&
|
|
2076
|
+
n.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken &&
|
|
2077
|
+
ts.isNumericLiteral(strip(n.right))
|
|
2078
|
+
)
|
|
2079
|
+
hit = true;
|
|
2080
|
+
ts.forEachChild(n, v);
|
|
2081
|
+
};
|
|
2082
|
+
v(expr);
|
|
2083
|
+
return hit;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function isLiteralList(init) {
|
|
2087
|
+
const n = strip(init);
|
|
2088
|
+
if (ts.isCallExpression(n) && dottedName(n.expression) === 'Array.from') {
|
|
2089
|
+
const first = strip(n.arguments[0]);
|
|
2090
|
+
const length =
|
|
2091
|
+
first && ts.isObjectLiteralExpression(first)
|
|
2092
|
+
? first.properties.find(p => p.name?.getText() === 'length')
|
|
2093
|
+
: null;
|
|
2094
|
+
return !!length && ts.isNumericLiteral(strip(length.initializer));
|
|
2095
|
+
}
|
|
2096
|
+
if ((ts.isCallExpression(n) || ts.isNewExpression(n)) && dottedName(n.expression) === 'Array')
|
|
2097
|
+
return n.arguments?.length === 1 && ts.isNumericLiteral(strip(n.arguments[0]));
|
|
2098
|
+
if (ts.isArrayLiteralExpression(n)) return isIndexList(n);
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
function checkString(node, text) {
|
|
2103
|
+
if (PULSE_CLASS.test(text))
|
|
2104
|
+
report(
|
|
2105
|
+
'OWN_PULSE',
|
|
2106
|
+
node,
|
|
2107
|
+
'a pulse or skeleton colour of its own (use the lib skeletons, LIVE_PULSE_CLASS for live dots)',
|
|
2108
|
+
);
|
|
2109
|
+
if (SPIN_CLASS.test(text)) checkSpin(node);
|
|
2110
|
+
const attrName = enclosingAttribute(node)?.name.getText() ?? '';
|
|
2111
|
+
const trimmed = text.trim();
|
|
2112
|
+
if (attrName === 'placeholder' && /^(?:loading|checking|fetching)\b/i.test(trimmed))
|
|
2113
|
+
report('LOADING_TEXT', node, `"${trimmed}" as a placeholder (draw the field in a loading scope)`);
|
|
2114
|
+
else if (
|
|
2115
|
+
WAIT_ELLIPSIS.test(trimmed) &&
|
|
2116
|
+
!/^(?:label|aria-label|aria-valuetext|title|alt|statusLabel|loadingLabel|placeholder)$/.test(attrName) &&
|
|
2117
|
+
!isAnnouncement(node) &&
|
|
2118
|
+
!inAnnouncement(node)
|
|
2119
|
+
)
|
|
2120
|
+
report('LOADING_TEXT', node, `"${trimmed}" as a loading state (draw the component skeleton)`);
|
|
2121
|
+
if (NTH_HIDDEN.test(text) && inLoadingContext(node, model))
|
|
2122
|
+
report(
|
|
2123
|
+
'HIDDEN_ITEMS',
|
|
2124
|
+
node,
|
|
2125
|
+
'loading items past the Nth hidden (a skeleton draws its full count at every width)',
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/** Inside a visually hidden status region (`role="status"`, `sr-only`): an announcement for assistive technology. */
|
|
2130
|
+
function inAnnouncement(node) {
|
|
2131
|
+
for (let p = node.parent; p && !isFunctionLike(p); p = p.parent) {
|
|
2132
|
+
const opening = ts.isJsxElement(p) ? p.openingElement : ts.isJsxSelfClosingElement(p) ? p : null;
|
|
2133
|
+
if (!opening) continue;
|
|
2134
|
+
const role = attributeExpression(attribute(opening, 'role'));
|
|
2135
|
+
const cls = attributeExpression(attribute(opening, 'className'));
|
|
2136
|
+
if ((role && /status|alert|log/.test(role.getText())) || (cls && /\bsr-only\b/.test(cls.getText()))) return true;
|
|
2137
|
+
}
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function isAnnouncement(node) {
|
|
2142
|
+
// `{ label: 'Loading…' }` handed to an announcing API, `aria` props spread.
|
|
2143
|
+
const p = node.parent;
|
|
2144
|
+
return (
|
|
2145
|
+
!!p && ts.isPropertyAssignment(p) && /^(?:label|ariaLabel|title|statusLabel|loadingLabel)$/.test(p.name.getText())
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function enclosingAttribute(node) {
|
|
2150
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
2151
|
+
if (ts.isJsxAttribute(p)) return p;
|
|
2152
|
+
if (ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p) || isFunctionLike(p) || ts.isStatement(p))
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function enclosingElement(node) {
|
|
2159
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
2160
|
+
if (ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p)) return p;
|
|
2161
|
+
if (isFunctionLike(p) || ts.isStatement(p)) return null;
|
|
2162
|
+
}
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function checkSpin(node) {
|
|
2167
|
+
const el = enclosingElement(node);
|
|
2168
|
+
const tag = el ? model.resolveName(el.tagName) : '';
|
|
2169
|
+
if (!PROGRESS_ICONS.has(tag)) {
|
|
2170
|
+
report(
|
|
2171
|
+
'SPINNER',
|
|
2172
|
+
node,
|
|
2173
|
+
'a spinner as a loading state (animate-spin only on an action progress icon: Loader2, RefreshCw)',
|
|
2174
|
+
);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
// A progress icon keyed on a READ flag, or returned as the whole output.
|
|
2178
|
+
const fn = componentOf(el);
|
|
2179
|
+
let child = ts.isJsxOpeningElement(el) ? el.parent : el;
|
|
2180
|
+
for (let p = child.parent; p && p !== fn; child = p, p = p.parent) {
|
|
2181
|
+
let test = null;
|
|
2182
|
+
if (ts.isConditionalExpression(p) && child !== p.condition) test = p.condition;
|
|
2183
|
+
if (
|
|
2184
|
+
ts.isBinaryExpression(p) &&
|
|
2185
|
+
p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
2186
|
+
child === p.right
|
|
2187
|
+
)
|
|
2188
|
+
test = p.left;
|
|
2189
|
+
if (ts.isIfStatement(p) && child !== p.expression) test = p.expression;
|
|
2190
|
+
if (test && readFlagIn(test, fn)) {
|
|
2191
|
+
report('SPINNER', node, 'a spinner as a loading state (a progress icon belongs to an action, never to a read)');
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (
|
|
2195
|
+
ts.isReturnStatement(p) &&
|
|
2196
|
+
nearestFunction(p) === fn &&
|
|
2197
|
+
fn &&
|
|
2198
|
+
isComponentName(functionName(fn)) &&
|
|
2199
|
+
rootElement(p.expression) === (ts.isJsxOpeningElement(el) ? el.parent : el)
|
|
2200
|
+
) {
|
|
2201
|
+
report('SPINNER', node, 'a spinner as a loading state (a progress icon belongs to an action, never to a read)');
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (ts.isJsxElement(p) && p !== child) break; // inside another element (a Button): an action's icon
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/** Whether a condition reads a READ flag (a hook's `loading`, `isLoading`, `isFetching`…), not an action's. */
|
|
2209
|
+
function readFlagIn(test, fn) {
|
|
2210
|
+
let hit = false;
|
|
2211
|
+
const v = (n, depth = 0) => {
|
|
2212
|
+
if (hit) return;
|
|
2213
|
+
if ((ts.isIdentifier(n) || ts.isPropertyAccessExpression(n)) && READ_FLAG.test(leafName(n))) {
|
|
2214
|
+
if (leafName(n) !== 'loading') hit = true;
|
|
2215
|
+
else if (ts.isIdentifier(n)) {
|
|
2216
|
+
const decl = model.lookup(n.text, n);
|
|
2217
|
+
if (decl?.kind === 'var' && decl.init && ts.isCallExpression(strip(decl.init))) hit = true;
|
|
2218
|
+
} else hit = true;
|
|
2219
|
+
if (hit) return;
|
|
2220
|
+
}
|
|
2221
|
+
// A local holding a read flag (`const busy = q.isLoading`).
|
|
2222
|
+
if (ts.isIdentifier(n) && depth < 3) {
|
|
2223
|
+
const decl = model.lookup(n.text, n);
|
|
2224
|
+
if (decl?.kind === 'var' && decl.init && !decl.destructured && !ts.isCallExpression(strip(decl.init)))
|
|
2225
|
+
v(decl.init, depth + 1);
|
|
2226
|
+
if (hit) return;
|
|
2227
|
+
}
|
|
2228
|
+
if (ts.isBinaryExpression(n) && isLoadingFlag(n, model) && /===|==/.test(n.operatorToken.getText())) hit = true;
|
|
2229
|
+
ts.forEachChild(n, c => v(c, depth));
|
|
2230
|
+
};
|
|
2231
|
+
v(test);
|
|
2232
|
+
return hit && !!fn;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
function checkElement(opening) {
|
|
2236
|
+
const tag = model.resolveName(opening.tagName);
|
|
2237
|
+
const leaf = tag.split('.').pop();
|
|
2238
|
+
skeletonRender(opening, opening.tagName);
|
|
2239
|
+
if (LOADERS.has(leaf)) report('LOADER_COMPONENT', opening, RULES.LOADER_COMPONENT);
|
|
2240
|
+
if (leaf === 'StreamedPage') {
|
|
2241
|
+
for (const a of opening.attributes.properties) {
|
|
2242
|
+
if (ts.isJsxAttribute(a) && /^(?:skeleton|memory|remember)$/.test(a.name.getText()))
|
|
2243
|
+
report('RETIRED_API', a, `StreamedPage \`${a.name.getText()}\` (it takes a placeholder)`);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (leaf === 'Suspense' || leaf === 'LoadingSuspense') checkFallback(opening, leaf);
|
|
2247
|
+
if (leaf === 'LoadingFallback' && ts.isJsxOpeningElement(opening)) {
|
|
2248
|
+
const children = opening.parent.children.filter(c => !(ts.isJsxText(c) && !c.text.trim()));
|
|
2249
|
+
if (children.every(c => (ts.isJsxExpression(c) ? !c.expression || isBlank(c.expression) : isBlank(c))))
|
|
2250
|
+
report('SUSPENSE_FALLBACK', opening, 'a LoadingFallback drawing nothing (render the view it stands for)');
|
|
2251
|
+
}
|
|
2252
|
+
if (leaf === 'LoadingFallback' && ts.isJsxSelfClosingElement(opening) && !attribute(opening, 'children'))
|
|
2253
|
+
report('SUSPENSE_FALLBACK', opening, 'a LoadingFallback drawing nothing (render the view it stands for)');
|
|
2254
|
+
if (leaf === 'LoadingRegion' && ts.isJsxOpeningElement(opening)) {
|
|
2255
|
+
let inner = '';
|
|
2256
|
+
walk(opening.parent, n => {
|
|
2257
|
+
const name = renderedTag(n) && model.resolveName(renderedTag(n));
|
|
2258
|
+
if (name === 'LoadingScope' || name === 'LoadingWhen') inner = name;
|
|
2259
|
+
});
|
|
2260
|
+
if (inner) report('HAND_PAIR', opening, `a hand LoadingRegion + ${inner} pair (use LoadingFallback)`);
|
|
2261
|
+
}
|
|
2262
|
+
if (PRIMITIVE.test(tag)) checkPrimitiveInBranch(opening, tag);
|
|
2263
|
+
// Counts on loading elements.
|
|
2264
|
+
for (const a of opening.attributes.properties) {
|
|
2265
|
+
if (!ts.isJsxAttribute(a)) continue;
|
|
2266
|
+
const name = a.name.getText();
|
|
2267
|
+
const value = attributeExpression(a);
|
|
2268
|
+
if (!value) continue;
|
|
2269
|
+
if (!isCountAttribute(tag, name)) continue;
|
|
2270
|
+
const kind = classifyCount(value, model, { attr: name });
|
|
2271
|
+
if (kind === 'typed')
|
|
2272
|
+
report(
|
|
2273
|
+
'TYPED_COUNT',
|
|
2274
|
+
a,
|
|
2275
|
+
`a typed skeleton count (${name}); name the list's page size, limit or a *_SKELETON_* constant`,
|
|
2276
|
+
);
|
|
2277
|
+
else if (kind === 'cap')
|
|
2278
|
+
report(
|
|
2279
|
+
'COUNT_CAP',
|
|
2280
|
+
a,
|
|
2281
|
+
`a capped skeleton count (${name}={${value.getText()}}); a list's skeleton draws its full page`,
|
|
2282
|
+
);
|
|
2283
|
+
else if (kind === 'unnamed')
|
|
2284
|
+
report(
|
|
2285
|
+
'COUNT_SOURCE',
|
|
2286
|
+
a,
|
|
2287
|
+
`${name}={${value.getText()}} does not name a page size, a limit or a *_SKELETON_* constant; name where the count comes from`,
|
|
2288
|
+
);
|
|
2289
|
+
else if (pagedSkeletonCount(value, a)) reportPagedSkeletonCount(a, value);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/** A `Suspense` / `LoadingSuspense` fallback that draws nothing (LoadingSuspense's own default is its children). */
|
|
2294
|
+
function checkFallback(opening, leaf) {
|
|
2295
|
+
const fallback = attribute(opening, 'fallback');
|
|
2296
|
+
const value = attributeExpression(fallback);
|
|
2297
|
+
if (!fallback) {
|
|
2298
|
+
if (leaf === 'Suspense')
|
|
2299
|
+
report('SUSPENSE_FALLBACK', opening, 'a Suspense with no fallback (use LoadingSuspense / LoadingFallback)');
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (!value || isBlank(value)) {
|
|
2303
|
+
report(
|
|
2304
|
+
'SUSPENSE_FALLBACK',
|
|
2305
|
+
value ?? fallback,
|
|
2306
|
+
`a ${leaf} fallback that draws nothing (draw the view it waits on)`,
|
|
2307
|
+
);
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const root = rootElement(value);
|
|
2311
|
+
const rootOpening = root && openingOf(root);
|
|
2312
|
+
if (leaf === 'Suspense' && rootOpening && model.resolveName(rootOpening.tagName) === 'LoadingFallback')
|
|
2313
|
+
report('HAND_PAIR', opening, 'a hand Suspense + LoadingFallback pair (use LoadingSuspense)');
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* A skeleton primitive drawn in the loading branch of `loading ? <Bars /> : <Y />` where Y is a component: the parent
|
|
2318
|
+
* draws Y's loading state (Y renders itself in the scope).
|
|
2319
|
+
*/
|
|
2320
|
+
function checkPrimitiveInBranch(opening, tag) {
|
|
2321
|
+
if (enclosingNames(opening).some(isSkeletonFunctionName)) return;
|
|
2322
|
+
const el = ts.isJsxOpeningElement(opening) ? opening.parent : opening;
|
|
2323
|
+
let child = el;
|
|
2324
|
+
let p = el.parent;
|
|
2325
|
+
while (p && ts.isParenthesizedExpression(p)) [child, p] = [p, p.parent];
|
|
2326
|
+
if (!p || !ts.isConditionalExpression(p) || child === p.condition) return;
|
|
2327
|
+
const loadingTrue = isLoadingFlag(p.condition, model);
|
|
2328
|
+
const loadingFalse = isNegatedLoading(p.condition, model);
|
|
2329
|
+
const loadingHalf = loadingTrue ? p.whenTrue : loadingFalse ? p.whenFalse : null;
|
|
2330
|
+
if (!loadingHalf || strip(loadingHalf) !== el) return;
|
|
2331
|
+
const other = rootElement(loadingTrue ? p.whenFalse : p.whenTrue);
|
|
2332
|
+
const otherOpening = other && openingOf(other);
|
|
2333
|
+
if (!otherOpening) return;
|
|
2334
|
+
const component = model.resolveName(otherOpening.tagName);
|
|
2335
|
+
if (
|
|
2336
|
+
!isComponentName(component) ||
|
|
2337
|
+
component.includes('.') ||
|
|
2338
|
+
isSkeletonName(component) ||
|
|
2339
|
+
LOADING_WRAPPER.test(component)
|
|
2340
|
+
)
|
|
2341
|
+
return;
|
|
2342
|
+
if (!project.componentSwitches(component) && !project.declaredIn.get(`${component}Skeleton`)?.size) return;
|
|
2343
|
+
parentPicks(opening, component, `<${tag.split('.')[0]}> drawn in place of <${component}> while loading`);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/** Whether a `*_SKELETON_*` count is drawn by a component whose list has a page size (it draws the page). */
|
|
2347
|
+
function pagedSkeletonCount(count, at) {
|
|
2348
|
+
const name = leafName(count);
|
|
2349
|
+
if (!/(?:^|_)SKELETON_/.test(name)) return false;
|
|
2350
|
+
const fn = componentOf(at);
|
|
2351
|
+
if (!fn?.body) return false;
|
|
2352
|
+
// The component's own read is paged: a page size handed to one of its hooks (`useQuery({ … PAGE_SIZE … })`).
|
|
2353
|
+
let paged = false;
|
|
2354
|
+
walk(fn.body, n => {
|
|
2355
|
+
if (paged || (isFunctionLike(n) && n !== fn)) return false;
|
|
2356
|
+
if (ts.isCallExpression(n) && /^use[A-Z]/.test(dottedName(n.expression).split('.').pop()))
|
|
2357
|
+
walk(n, m => {
|
|
2358
|
+
if ((ts.isIdentifier(m) || ts.isPropertyAccessExpression(m)) && PAGE_SIZE_NAME.test(leafName(m)))
|
|
2359
|
+
paged = true;
|
|
2360
|
+
if (ts.isPropertyAssignment(m) && PAGE_SIZE_NAME.test(m.name.getText())) paged = true;
|
|
2361
|
+
});
|
|
2362
|
+
return undefined;
|
|
2363
|
+
});
|
|
2364
|
+
return paged;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function reportPagedSkeletonCount(at, count) {
|
|
2368
|
+
report(
|
|
2369
|
+
'COUNT_SOURCE',
|
|
2370
|
+
at,
|
|
2371
|
+
`${count.getText()} is a *_SKELETON_* count in a component whose list has a page size (a paged list draws its page size)`,
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function checkAttribute(attr) {
|
|
2376
|
+
const name = attr.name.getText();
|
|
2377
|
+
if (name === 'fold') report('FOLD_PROP', attr, 'a `fold` prop (a skeleton draws its full count: skeletonCount)');
|
|
2378
|
+
// `{YSkeleton}` handed to a prop is a parent choosing the child's skeleton.
|
|
2379
|
+
const value = attributeExpression(attr);
|
|
2380
|
+
if (value && ts.isIdentifier(strip(value))) skeletonRender(attr, value);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function checkList({ count, callback, node, loadingItems, literalArray }) {
|
|
2384
|
+
const item = callbackItem(callback);
|
|
2385
|
+
const countName = count && !literalArray ? leafName(count) : '';
|
|
2386
|
+
// Text: a typical line count, when each item is a skeleton line itself (a list of rows named `*LINES` is a list).
|
|
2387
|
+
const textLines = isTextLineCount(countName);
|
|
2388
|
+
if (textLines && (!item || lineItem(item))) return;
|
|
2389
|
+
const loading = loadingItems || inLoadingContext(node, model);
|
|
2390
|
+
// The cells of one row (`columns`, `cells`) are not list items: their count never changes the row's height.
|
|
2391
|
+
const cells = isCellCount(countName);
|
|
2392
|
+
if (loading && count && !loadingItems && !cells) {
|
|
2393
|
+
const kind = literalArray ? 'typed' : classifyCount(count, model);
|
|
2394
|
+
if (kind === 'typed')
|
|
2395
|
+
report(
|
|
2396
|
+
'TYPED_COUNT',
|
|
2397
|
+
node,
|
|
2398
|
+
`a typed count in ${enclosingNames(node).find(Boolean) || 'a loading branch'}; name the list's page size, limit or a *_SKELETON_* constant`,
|
|
2399
|
+
);
|
|
2400
|
+
else if (kind === 'cap')
|
|
2401
|
+
report('COUNT_CAP', node, `a capped skeleton list (${count.getText()}); a list's skeleton draws its full page`);
|
|
2402
|
+
else if (kind === 'unnamed')
|
|
2403
|
+
report(
|
|
2404
|
+
'COUNT_SOURCE',
|
|
2405
|
+
node,
|
|
2406
|
+
`a skeleton list of ${count.getText()} items; name the page size, limit or a *_SKELETON_* constant`,
|
|
2407
|
+
);
|
|
2408
|
+
else if (pagedSkeletonCount(count, node)) reportPagedSkeletonCount(node, count);
|
|
2409
|
+
}
|
|
2410
|
+
const passedCount = loading && (COUNT_ATTR.test(countName) || PASSED_COUNT.test(countName));
|
|
2411
|
+
const isList = !cells && (loadingItems || textLines || passedCount || COUNT_SOURCE_NAME.test(countName));
|
|
2412
|
+
if (loading) checkHiddenItems(callback, item);
|
|
2413
|
+
if (!isList) return;
|
|
2414
|
+
if (item && !itemMarked(item, model))
|
|
2415
|
+
report(
|
|
2416
|
+
'ITEM_MARK',
|
|
2417
|
+
node,
|
|
2418
|
+
`a skeleton list of ${count ? count.getText() : 'N'} items with no data-loading-item (or loadingItemProps) on its items`,
|
|
2419
|
+
);
|
|
2420
|
+
const loaded = loadedListBeside(node, model);
|
|
2421
|
+
if (loaded) {
|
|
2422
|
+
const loadedItem = callbackItem(loaded.arguments[0]);
|
|
2423
|
+
if (loadedItem && !itemMarked(loadedItem, model))
|
|
2424
|
+
report('ITEM_MARK', loaded, 'the loaded list beside a skeleton list has no data-loading-item on its items');
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/** Whether a list item is one skeleton line (a primitive, or an element holding only one). */
|
|
2429
|
+
function lineItem(item) {
|
|
2430
|
+
const opening = openingOf(item);
|
|
2431
|
+
if (!opening) return false;
|
|
2432
|
+
if (PRIMITIVE.test(model.resolveName(opening.tagName))) return true;
|
|
2433
|
+
if (!ts.isJsxElement(item)) return false;
|
|
2434
|
+
const children = item.children.filter(c => !(ts.isJsxText(c) && !c.text.trim()));
|
|
2435
|
+
return (
|
|
2436
|
+
children.length === 1 &&
|
|
2437
|
+
(ts.isJsxElement(children[0]) || ts.isJsxSelfClosingElement(children[0])) &&
|
|
2438
|
+
PRIMITIVE.test(model.resolveName(openingOf(children[0]).tagName)) &&
|
|
2439
|
+
!/^(?:li|tr)$/.test(opening.tagName.getText())
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/** Loading items hidden per breakpoint (their class, through a constant too), or returned as nothing past the Nth. */
|
|
2444
|
+
function checkHiddenItems(callback, item) {
|
|
2445
|
+
const opening = item && openingOf(item);
|
|
2446
|
+
const cls = opening && attributeExpression(attribute(opening, 'className'));
|
|
2447
|
+
if (cls && classStrings(cls, model).some(s => BREAKPOINT_HIDDEN.test(s)))
|
|
2448
|
+
report(
|
|
2449
|
+
'HIDDEN_ITEMS',
|
|
2450
|
+
cls,
|
|
2451
|
+
'a loading item hidden per breakpoint (a skeleton draws its full count at every width)',
|
|
2452
|
+
);
|
|
2453
|
+
const cb = strip(callback);
|
|
2454
|
+
if (!cb || !isFunctionLike(cb)) return;
|
|
2455
|
+
const index = cb.parameters.map(param => param.name.getText());
|
|
2456
|
+
const returned = strip(returnedExpression(cb));
|
|
2457
|
+
if (!returned) return;
|
|
2458
|
+
const cond = ts.isConditionalExpression(returned)
|
|
2459
|
+
? (isNullish(returned.whenTrue) || isNullish(returned.whenFalse)) && returned.condition
|
|
2460
|
+
: ts.isBinaryExpression(returned) && returned.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
2461
|
+
? returned.left
|
|
2462
|
+
: null;
|
|
2463
|
+
if (!cond) return;
|
|
2464
|
+
const byIndex = n => isComparison(n) && index.some(i => new RegExp(`\\b${i}\\b`).test(n.getText()));
|
|
2465
|
+
let hidden = byIndex(cond);
|
|
2466
|
+
walk(cond, n => {
|
|
2467
|
+
if (byIndex(n)) hidden = true;
|
|
2468
|
+
});
|
|
2469
|
+
if (hidden)
|
|
2470
|
+
report('HIDDEN_ITEMS', cond, 'loading items past the Nth drawn as nothing (a skeleton draws its full count)');
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// ---- nothing while loading --------------------------------------------------------------------------------------
|
|
2474
|
+
|
|
2475
|
+
function componentScope(node) {
|
|
2476
|
+
const fn = nearestFunction(node);
|
|
2477
|
+
if (!fn) return null;
|
|
2478
|
+
return isComponentName(functionName(fn)) && !isSkeletonFunctionName(functionName(fn)) ? fn : null;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function checkIfNull(stmt) {
|
|
2482
|
+
if (!isTsx) return;
|
|
2483
|
+
const then = stmt.thenStatement;
|
|
2484
|
+
const ret = ts.isReturnStatement(then)
|
|
2485
|
+
? then
|
|
2486
|
+
: ts.isBlock(then) && then.statements.length === 1 && ts.isReturnStatement(then.statements[0])
|
|
2487
|
+
? then.statements[0]
|
|
2488
|
+
: null;
|
|
2489
|
+
if (!ret || !(ret.expression && isBlank(ret.expression))) return;
|
|
2490
|
+
const fn = componentScope(stmt);
|
|
2491
|
+
if (!fn) return;
|
|
2492
|
+
const test = stmt.expression;
|
|
2493
|
+
if (isScopeCall(test) && (delegatesToReader(fn) || returnsOptional(fn))) return;
|
|
2494
|
+
// A part some records lack (its loaded return marked `data-loading-optional`): absent while loading too.
|
|
2495
|
+
if (!mentionsLoading(test) && returnsOptional(fn)) return;
|
|
2496
|
+
if (isLoadingFlag(test, model) || (heldWhileLoading(test, model) && mentionsLoading(test))) {
|
|
2497
|
+
report('NOTHING_WHILE_LOADING', stmt, 'nothing drawn while loading (render the component in a loading scope)');
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (heldWhileLoading(test, model) && !answeredBefore(fn, stmt, model))
|
|
2501
|
+
report(
|
|
2502
|
+
'NOTHING_WHILE_LOADING',
|
|
2503
|
+
stmt,
|
|
2504
|
+
'nothing drawn while the read is pending (draw the component in a loading scope)',
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function isScopeCall(test) {
|
|
2509
|
+
const n = strip(test);
|
|
2510
|
+
return ts.isCallExpression(n) && project.scopeHooks.has(dottedName(n.expression).split('.').pop());
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/** A Relay container's split (`if (useLoadingScope()) return null; return <XQuery {...props} />`): the reader cannot
|
|
2514
|
+
* run in a scope, and its view is drawn by the boundary's own fallback. */
|
|
2515
|
+
function delegatesToReader(fn) {
|
|
2516
|
+
let reader = false;
|
|
2517
|
+
walk(fn.body ?? fn, n => {
|
|
2518
|
+
if (isFunctionLike(n)) return false;
|
|
2519
|
+
if (ts.isReturnStatement(n) && n.expression) {
|
|
2520
|
+
const el = rootElement(n.expression);
|
|
2521
|
+
const opening = el && openingOf(el);
|
|
2522
|
+
if (opening && /(?:Query|Reader|Container|Loader)$/.test(dottedName(opening.tagName))) reader = true;
|
|
2523
|
+
}
|
|
2524
|
+
return undefined;
|
|
2525
|
+
});
|
|
2526
|
+
return reader;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/** Whether every JSX the function returns is marked `data-loading-optional` (a part some records lack). */
|
|
2530
|
+
function returnsOptional(fn) {
|
|
2531
|
+
let marked = false;
|
|
2532
|
+
let unmarked = false;
|
|
2533
|
+
walk(fn.body ?? fn, n => {
|
|
2534
|
+
if (isFunctionLike(n)) return false;
|
|
2535
|
+
if (ts.isReturnStatement(n) && n.expression && !isNullish(n.expression)) {
|
|
2536
|
+
const el = rootElement(n.expression);
|
|
2537
|
+
const opening = el && openingOf(el);
|
|
2538
|
+
if (opening && attribute(opening, 'data-loading-optional')) marked = true;
|
|
2539
|
+
else unmarked = true;
|
|
2540
|
+
}
|
|
2541
|
+
return undefined;
|
|
2542
|
+
});
|
|
2543
|
+
return marked && !unmarked;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/** Whether `flag` drives an action in this component (`loading` / `disabled` of a `*Button`): an action's progress. */
|
|
2547
|
+
function actionFlag(fn, flag) {
|
|
2548
|
+
let hit = false;
|
|
2549
|
+
walk(fn.body ?? fn, n => {
|
|
2550
|
+
if (hit) return false;
|
|
2551
|
+
if ((ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) && /Button$/.test(dottedName(n.tagName))) {
|
|
2552
|
+
for (const a of n.attributes.properties) {
|
|
2553
|
+
const v =
|
|
2554
|
+
ts.isJsxAttribute(a) && /^(?:loading|isLoading|disabled)$/.test(a.name.getText()) && attributeExpression(a);
|
|
2555
|
+
if (v && strip(v).getText() === flag) hit = true;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return undefined;
|
|
2559
|
+
});
|
|
2560
|
+
return hit;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
function mentionsLoading(test) {
|
|
2564
|
+
let hit = false;
|
|
2565
|
+
const v = n => {
|
|
2566
|
+
if (hit) return;
|
|
2567
|
+
if (isLoadingFlag(n, model) && !(ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.BarBarToken))
|
|
2568
|
+
hit = true;
|
|
2569
|
+
ts.forEachChild(n, v);
|
|
2570
|
+
};
|
|
2571
|
+
v(test);
|
|
2572
|
+
return hit;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function checkTernaryNull(cond) {
|
|
2576
|
+
if (inAttributeValue(cond)) return;
|
|
2577
|
+
const nullTrue = isNullish(cond.whenTrue);
|
|
2578
|
+
const nullFalse = isNullish(cond.whenFalse);
|
|
2579
|
+
if (nullTrue === nullFalse) return;
|
|
2580
|
+
const other = nullTrue ? cond.whenFalse : cond.whenTrue;
|
|
2581
|
+
if (!isJsx(other) && !inRenderPosition(cond)) return;
|
|
2582
|
+
const fn = componentScope(cond);
|
|
2583
|
+
if (!fn && !ts.isSourceFile(nearestFunctionOrFile(cond))) return;
|
|
2584
|
+
const whole = isWholeOutput(cond, fn);
|
|
2585
|
+
const opts = { hookOnly: !whole };
|
|
2586
|
+
const empty = nullTrue
|
|
2587
|
+
? heldWhileLoading(cond.condition, model, opts)
|
|
2588
|
+
: falseWhileLoading(cond.condition, model, opts);
|
|
2589
|
+
if (!empty) return;
|
|
2590
|
+
const flag = nullTrue ? mentionsLoading(cond.condition) : isNegatedLoadingIn(cond.condition);
|
|
2591
|
+
if (!flag && fn && answeredBefore(fn, cond, model)) return;
|
|
2592
|
+
if (optionalHandled(cond, other) || offScreen(other)) return;
|
|
2593
|
+
if (!nullTrue && flag === false && switchingComponent(cond)) return; // a read-keyed part: OPTIONAL_MARK's
|
|
2594
|
+
if (flag && !nullTrue && switchingComponent(cond)) return;
|
|
2595
|
+
report(
|
|
2596
|
+
'NOTHING_WHILE_LOADING',
|
|
2597
|
+
cond,
|
|
2598
|
+
flag
|
|
2599
|
+
? 'nothing drawn while loading (render the component in a loading scope)'
|
|
2600
|
+
: 'nothing drawn while the read is pending (draw the component in a loading scope)',
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
/** Whether a node is (inside) an attribute's value, not a rendered child. */
|
|
2605
|
+
function inAttributeValue(node) {
|
|
2606
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
2607
|
+
if (ts.isJsxAttribute(p)) return true;
|
|
2608
|
+
if (isJsxChild(p) || ts.isStatement(p) || isFunctionLike(p)) return false;
|
|
2609
|
+
}
|
|
2610
|
+
return false;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
/** The flags a condition negates (`!loading` -> `loading`). */
|
|
2614
|
+
function negatedFlags(test) {
|
|
2615
|
+
const out = [];
|
|
2616
|
+
const v = n => {
|
|
2617
|
+
const x = strip(n);
|
|
2618
|
+
if (isNegated(x) && isLoadingFlag(x.operand, model)) out.push(strip(x.operand).getText());
|
|
2619
|
+
else if (ts.isBinaryExpression(x) && x.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
2620
|
+
v(x.left);
|
|
2621
|
+
v(x.right);
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
v(test);
|
|
2625
|
+
return out;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function isNegatedLoadingIn(test) {
|
|
2629
|
+
const n = strip(test);
|
|
2630
|
+
if (isNegatedLoading(n, model)) return true;
|
|
2631
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
2632
|
+
return isNegatedLoadingIn(n.left) || isNegatedLoadingIn(n.right);
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function nearestFunctionOrFile(node) {
|
|
2637
|
+
return nearestFunction(node) ?? node.getSourceFile();
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function inRenderPosition(node) {
|
|
2641
|
+
let p = node.parent;
|
|
2642
|
+
while (p && ts.isParenthesizedExpression(p)) p = p.parent;
|
|
2643
|
+
return !!p && (isJsxChild(p) || ts.isReturnStatement(p) || (ts.isArrowFunction(p) && p.body === node));
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
function isWholeOutput(node, fn) {
|
|
2647
|
+
let p = node.parent;
|
|
2648
|
+
while (p && ts.isParenthesizedExpression(p)) p = p.parent;
|
|
2649
|
+
if (!p || !fn) return false;
|
|
2650
|
+
return (
|
|
2651
|
+
(ts.isReturnStatement(p) && nearestFunction(p) === fn) ||
|
|
2652
|
+
(p === fn && fn.body === node) ||
|
|
2653
|
+
(ts.isArrowFunction(p) && p === fn)
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/** `{loading || <X />}`: while loading the slot renders the flag (nothing). */
|
|
2658
|
+
function checkOrPart(bin) {
|
|
2659
|
+
if (!isJsx(bin.right)) return;
|
|
2660
|
+
let p = bin.parent;
|
|
2661
|
+
while (p && ts.isParenthesizedExpression(p)) p = p.parent;
|
|
2662
|
+
if (!p || !(isJsxChild(p) || ts.isReturnStatement(p) || ts.isArrowFunction(p))) return;
|
|
2663
|
+
if (!heldWhileLoading(bin.left, model) || offScreen(bin.right)) return;
|
|
2664
|
+
report('NOTHING_WHILE_LOADING', bin, 'nothing drawn while loading (`loading || <X />` renders the flag)');
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
/** A `next/dynamic` import with no `loading:` fallback: its slot is empty while the chunk loads. */
|
|
2668
|
+
function checkDynamic(call) {
|
|
2669
|
+
const options = strip(call.arguments[1]);
|
|
2670
|
+
const hasLoading =
|
|
2671
|
+
options &&
|
|
2672
|
+
ts.isObjectLiteralExpression(options) &&
|
|
2673
|
+
options.properties.some(q => q.name?.getText() === 'loading' || ts.isSpreadAssignment(q));
|
|
2674
|
+
if (hasLoading || (options && !ts.isObjectLiteralExpression(options))) return;
|
|
2675
|
+
let p = call.parent;
|
|
2676
|
+
while (p && (ts.isParenthesizedExpression(p) || ts.isAsExpression(p) || ts.isCallExpression(p))) p = p.parent;
|
|
2677
|
+
const name = p && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name) ? p.name.text : '';
|
|
2678
|
+
if (OFF_SCREEN.test(name)) return;
|
|
2679
|
+
report(
|
|
2680
|
+
'NOTHING_WHILE_LOADING',
|
|
2681
|
+
call,
|
|
2682
|
+
`a next/dynamic import${name ? ` (${name})` : ''} with no loading: fallback (draw its skeleton while the chunk loads)`,
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
function checkAndPart(bin) {
|
|
2687
|
+
if (!isJsx(bin.right)) return;
|
|
2688
|
+
let p = bin.parent;
|
|
2689
|
+
while (p && ts.isParenthesizedExpression(p)) p = p.parent;
|
|
2690
|
+
if (!p || !(isJsxChild(p) || ts.isReturnStatement(p))) return;
|
|
2691
|
+
const fn = componentScope(bin);
|
|
2692
|
+
const scopeNode = fn ?? (nearestFunction(bin) ? null : bin.getSourceFile());
|
|
2693
|
+
if (!scopeNode) return;
|
|
2694
|
+
const test = bin.left;
|
|
2695
|
+
if (FAILURE.test(test.getText()) || emptyState(test, bin.right)) return;
|
|
2696
|
+
// A part drawn WHILE something loads (`isLoadingMore && !isLoading && <Card loading />`) is a loading form.
|
|
2697
|
+
if (conjuncts(test).some(c => isLoadingFlag(c, model))) return;
|
|
2698
|
+
const switching = switchingComponent(bin);
|
|
2699
|
+
const negLoading = isNegatedLoadingIn(test);
|
|
2700
|
+
const readKey = !negLoading && readFieldKey(test);
|
|
2701
|
+
if (!negLoading && !readKey) {
|
|
2702
|
+
// A read keyed on hook data, in a component that does not switch.
|
|
2703
|
+
// Every operand must be the read (an operand of the user's own state or a prop gates the part on that instead).
|
|
2704
|
+
if (
|
|
2705
|
+
!switching &&
|
|
2706
|
+
fn &&
|
|
2707
|
+
conjuncts(test).every(c => falseWhileLoading(c, model, { hookOnly: true })) &&
|
|
2708
|
+
!answeredBefore(fn, bin, model) &&
|
|
2709
|
+
!optionalHandled(bin, bin.right) &&
|
|
2710
|
+
!offScreen(bin.right)
|
|
2711
|
+
)
|
|
2712
|
+
report(
|
|
2713
|
+
'NOTHING_WHILE_LOADING',
|
|
2714
|
+
bin,
|
|
2715
|
+
'nothing drawn while the read is pending (draw the component in a loading scope)',
|
|
2716
|
+
);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (optionalHandled(bin, bin.right) || offScreen(bin.right) || skeletonOutput(bin.right)) return;
|
|
2720
|
+
if (inLoadedForm(bin)) return;
|
|
2721
|
+
if (negLoading && fn && negatedFlags(test).some(flag => actionFlag(fn, flag))) return;
|
|
2722
|
+
if (switching) {
|
|
2723
|
+
report('OPTIONAL_MARK', bin, 'a part left out while loading has no data-loading-optional');
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
if (negLoading)
|
|
2727
|
+
report('NOTHING_WHILE_LOADING', bin, 'nothing drawn while loading (render the component in a loading scope)');
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/** A part that takes no room in the page (a modal, a dialog, a toast, a portal): absent while loading is no jump. */
|
|
2731
|
+
function offScreen(element) {
|
|
2732
|
+
const el = rootElement(element);
|
|
2733
|
+
const opening = el && openingOf(el);
|
|
2734
|
+
if (!opening) return false;
|
|
2735
|
+
if (OFF_SCREEN.test(dottedName(opening.tagName))) return true;
|
|
2736
|
+
if (isComponentName(dottedName(opening.tagName)) && project.rendersNothing(model.path, dottedName(opening.tagName)))
|
|
2737
|
+
return true;
|
|
2738
|
+
// A visually hidden announcement (`sr-only`, `role="status"`) takes no room either.
|
|
2739
|
+
const role = attributeExpression(attribute(opening, 'role'));
|
|
2740
|
+
const cls = attributeExpression(attribute(opening, 'className'));
|
|
2741
|
+
return (!!role && /status|alert|log/.test(role.getText())) || (!!cls && /\bsr-only\b/.test(cls.getText()));
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
/** An empty or failed state (`!hasResults`, `items.length === 0`, `<EmptyState />`): a state, not a part of a record. */
|
|
2745
|
+
function emptyState(test, element) {
|
|
2746
|
+
if (/\.length\s*===?\s*0|!\s*has[A-Z]\w*|\bisEmpty\b|\bempty\b|\bno[A-Z]\w*/.test(test.getText())) return true;
|
|
2747
|
+
const el = rootElement(element);
|
|
2748
|
+
const opening = el && openingOf(el);
|
|
2749
|
+
return !!opening && /(?:Empty|Error|Failure|NotFound)\w*$/.test(dottedName(opening.tagName));
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
/** `x.y` / `x?.y` (a field of a record), not a flag-like field, not `props.flag`. */
|
|
2753
|
+
function readFieldKey(test) {
|
|
2754
|
+
const n = strip(test);
|
|
2755
|
+
if (!n || !(ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n))) return false;
|
|
2756
|
+
if (ts.isPropertyAccessExpression(n) && (FLAG_FIELD.test(n.name.text) || CONFIG_FIELD.test(n.name.text)))
|
|
2757
|
+
return false;
|
|
2758
|
+
const root = chainRoot(n);
|
|
2759
|
+
if (!root) return false;
|
|
2760
|
+
if (LOADING_FLAG.test(root.text) || CONFIG_ROOT.test(root.text)) return false;
|
|
2761
|
+
return true;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
/** Marked optional, the loaded half of an explicit pair, or inside a per-item callback. */
|
|
2765
|
+
function optionalHandled(expr, element) {
|
|
2766
|
+
const el = rootElement(element);
|
|
2767
|
+
const opening = el && openingOf(el);
|
|
2768
|
+
if (opening && attribute(opening, 'data-loading-optional')) return true;
|
|
2769
|
+
if (
|
|
2770
|
+
opening &&
|
|
2771
|
+
isComponentName(dottedName(opening.tagName)) &&
|
|
2772
|
+
model.project.componentMarks(model.path, dottedName(opening.tagName).split('.')[0], 'optional')
|
|
2773
|
+
)
|
|
2774
|
+
return true;
|
|
2775
|
+
// Inside a list's per-item callback (`items.map(item => item.x && …)`).
|
|
2776
|
+
const fn = nearestFunction(expr);
|
|
2777
|
+
if (fn && !isComponentName(functionName(fn)) && ts.isCallExpression(fn.parent)) return true;
|
|
2778
|
+
// The loaded half of an explicit pair: the sibling just before it draws the slot's loading half
|
|
2779
|
+
// (`{loading && <Bar />}` or `{loading ? <Bar /> : null}`).
|
|
2780
|
+
let container = expr.parent;
|
|
2781
|
+
while (container && ts.isParenthesizedExpression(container)) container = container.parent;
|
|
2782
|
+
if (
|
|
2783
|
+
container &&
|
|
2784
|
+
ts.isJsxExpression(container) &&
|
|
2785
|
+
(ts.isJsxElement(container.parent) || ts.isJsxFragment(container.parent))
|
|
2786
|
+
) {
|
|
2787
|
+
const siblings = container.parent.children.filter(c => !(ts.isJsxText(c) && !c.text.trim()));
|
|
2788
|
+
// Walk back over the loaded alternatives of the same slot (`{!loading && a && …}`, `{!loading && !a && …}`).
|
|
2789
|
+
for (let i = siblings.indexOf(container) - 1; i >= 0; i--) {
|
|
2790
|
+
const before = siblings[i];
|
|
2791
|
+
if (!ts.isJsxExpression(before) || !before.expression) break;
|
|
2792
|
+
const s = strip(before.expression);
|
|
2793
|
+
if (
|
|
2794
|
+
ts.isBinaryExpression(s) &&
|
|
2795
|
+
s.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
2796
|
+
isLoadingFlag(s.left, model)
|
|
2797
|
+
)
|
|
2798
|
+
return true;
|
|
2799
|
+
if (ts.isConditionalExpression(s) && isLoadingFlag(s.condition, model) && isNullish(s.whenFalse)) return true;
|
|
2800
|
+
const alternative =
|
|
2801
|
+
(ts.isBinaryExpression(s) && isNegatedLoadingIn(s.left)) ||
|
|
2802
|
+
(ts.isConditionalExpression(s) && isNegatedLoadingIn(s.condition));
|
|
2803
|
+
if (!alternative) break;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return false;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
/**
|
|
2810
|
+
* Whether a part sits where the loading form draws its OWN version of the region: after an early
|
|
2811
|
+
* `if (loading) return …`, in the loaded half of `loading ? <A /> : <B />` (two forms of one slot), or inside a
|
|
2812
|
+
* popover / dialog that is never part of the page's loading form. Only a part of the one structure both forms share
|
|
2813
|
+
* can be left out while loading.
|
|
2814
|
+
*/
|
|
2815
|
+
function inLoadedForm(node) {
|
|
2816
|
+
const fn = componentOf(node);
|
|
2817
|
+
let child = node;
|
|
2818
|
+
for (let p = node.parent; p && p !== fn; child = p, p = p.parent) {
|
|
2819
|
+
// `if (loading) { body = <bars /> } else { body = <loaded /> }`: the else is the loaded form.
|
|
2820
|
+
if (ts.isIfStatement(p) && child === p.elseStatement && heldWhileLoading(p.expression, model)) return true;
|
|
2821
|
+
if (ts.isConditionalExpression(p) && child !== p.condition) {
|
|
2822
|
+
const loadingTrue = isLoadingFlag(p.condition, model);
|
|
2823
|
+
const loadingFalse = isNegatedLoading(p.condition, model);
|
|
2824
|
+
const loadedHalf = loadingTrue ? p.whenFalse : loadingFalse ? p.whenTrue : null;
|
|
2825
|
+
const loadingHalf = loadingTrue ? p.whenTrue : p.whenFalse;
|
|
2826
|
+
if (loadedHalf && loadedHalf === child && !isNullish(loadingHalf)) return true;
|
|
2827
|
+
}
|
|
2828
|
+
if (
|
|
2829
|
+
ts.isJsxElement(p) &&
|
|
2830
|
+
/(?:HoverCard|Popover|Tooltip|DropdownMenu|Menu)\w*Content$|(?:Modal|Dialog|Drawer|Sheet)$/.test(
|
|
2831
|
+
dottedName(p.openingElement.tagName),
|
|
2832
|
+
)
|
|
2833
|
+
)
|
|
2834
|
+
return true;
|
|
2835
|
+
}
|
|
2836
|
+
if (!fn?.body || !ts.isBlock(fn.body)) return false;
|
|
2837
|
+
const pos = node.getStart();
|
|
2838
|
+
return fn.body.statements.some(
|
|
2839
|
+
st =>
|
|
2840
|
+
ts.isIfStatement(st) &&
|
|
2841
|
+
st.getEnd() <= pos &&
|
|
2842
|
+
isLoadingFlag(st.expression, model) &&
|
|
2843
|
+
containsReturn(st.thenStatement),
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
/** Whether a function draws a loading form (a skeleton, a `loading` handed down, `LoadingItems`, `LoadingWhen`). */
|
|
2848
|
+
function hasLoadingForm(fn) {
|
|
2849
|
+
let hit = false;
|
|
2850
|
+
walk(fn.body ?? fn, n => {
|
|
2851
|
+
if (hit) return false;
|
|
2852
|
+
if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
|
|
2853
|
+
const tag = model.resolveName(n.tagName);
|
|
2854
|
+
if (isSkeletonName(tag) || /^Loading(?:Items|When|Fallback|Suspense)$/.test(tag)) hit = true;
|
|
2855
|
+
if (n.attributes.properties.some(a => ts.isJsxAttribute(a) && /^(?:loading|isLoading)$/.test(a.name.getText())))
|
|
2856
|
+
hit = true;
|
|
2857
|
+
}
|
|
2858
|
+
if (ts.isCallExpression(n) && /loadingItemProps$/.test(dottedName(n.expression))) hit = true;
|
|
2859
|
+
// A branch on the loading flag (`isLoading ? <bars> : rows.map(…)`) is a loading form drawn in place.
|
|
2860
|
+
if (ts.isConditionalExpression(n) && (isLoadingFlag(n.condition, model) || isNegatedLoading(n.condition, model)))
|
|
2861
|
+
hit = true;
|
|
2862
|
+
if (ts.isIfStatement(n) && isLoadingFlag(n.expression, model)) hit = true;
|
|
2863
|
+
return undefined;
|
|
2864
|
+
});
|
|
2865
|
+
return hit;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function switchingComponent(node) {
|
|
2869
|
+
const fn = componentOf(node);
|
|
2870
|
+
if (fn) return project.callsScope(fn) && hasLoadingForm(fn);
|
|
2871
|
+
// Module-level code (a fixture snippet): the file reads the scope.
|
|
2872
|
+
return /\buseLoadingScope\s*\(/.test(model.sf.text);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// ---- the walk ---------------------------------------------------------------------------------------------------
|
|
2876
|
+
visit(model.sf);
|
|
2877
|
+
|
|
2878
|
+
// ---- per component: optional parts on read fields as ternaries, and the prop-only switch ---------------------------
|
|
2879
|
+
for (const fn of components) checkComponent(fn);
|
|
2880
|
+
checkReadTernaries(model.sf);
|
|
2881
|
+
|
|
2882
|
+
function checkReadTernaries(scope) {
|
|
2883
|
+
walk(scope, n => {
|
|
2884
|
+
if (ts.isConditionalExpression(n)) checkReadTernary(n);
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function checkReadTernary(cond) {
|
|
2889
|
+
if (inAttributeValue(cond)) return;
|
|
2890
|
+
// A link of a chain (`a ? <A /> : b ? <B /> : null`) chooses between forms of one slot.
|
|
2891
|
+
if (
|
|
2892
|
+
ts.isConditionalExpression(strip(cond.parent) ?? cond.parent) ||
|
|
2893
|
+
(cond.parent && ts.isConditionalExpression(cond.parent))
|
|
2894
|
+
)
|
|
2895
|
+
return;
|
|
2896
|
+
const nullTrue = isNullish(cond.whenTrue);
|
|
2897
|
+
const nullFalse = isNullish(cond.whenFalse);
|
|
2898
|
+
if (!nullFalse || nullTrue || !isJsx(cond.whenTrue)) return;
|
|
2899
|
+
if (
|
|
2900
|
+
!readFieldKey(cond.condition) ||
|
|
2901
|
+
FAILURE.test(cond.condition.getText()) ||
|
|
2902
|
+
emptyState(cond.condition, cond.whenTrue)
|
|
2903
|
+
)
|
|
2904
|
+
return;
|
|
2905
|
+
if (!switchingComponent(cond) || optionalHandled(cond, cond.whenTrue) || inLoadedForm(cond)) return;
|
|
2906
|
+
if (componentScope(cond) == null && nearestFunction(cond)) return;
|
|
2907
|
+
report('OPTIONAL_MARK', cond, 'a part left out while loading has no data-loading-optional');
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
function checkComponent(fn) {
|
|
2911
|
+
const switches = project.callsScope(fn);
|
|
2912
|
+
if (switches) return;
|
|
2913
|
+
// PROP-ONLY SWITCH: a skeleton drawn on a boolean that comes from the component's own props / locals.
|
|
2914
|
+
walk(fn.body ?? fn, n => {
|
|
2915
|
+
if (isFunctionLike(n) && n !== fn) return false;
|
|
2916
|
+
let test = null;
|
|
2917
|
+
let branch = null;
|
|
2918
|
+
if (ts.isIfStatement(n)) [test, branch] = [n.expression, n.thenStatement];
|
|
2919
|
+
else if (ts.isConditionalExpression(n))
|
|
2920
|
+
[test, branch] = [n.condition, isSkeletonOutput(n.whenTrue) ? n.whenTrue : n.whenFalse];
|
|
2921
|
+
else if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)
|
|
2922
|
+
[test, branch] = [n.left, n.right];
|
|
2923
|
+
if (!test || !branch) return undefined;
|
|
2924
|
+
const skeleton = skeletonOutput(branch);
|
|
2925
|
+
if (!skeleton || oneSwitchSites.has(skeleton)) return undefined;
|
|
2926
|
+
if (propDerived(test, fn) && readsFlag(test, fn))
|
|
2927
|
+
report('PROP_ONLY_SWITCH', n, 'a switch on the loading prop alone (switch on useLoadingScope() || loading)');
|
|
2928
|
+
else if (ownReadFlag(test, fn))
|
|
2929
|
+
report(
|
|
2930
|
+
'PROP_ONLY_SWITCH',
|
|
2931
|
+
n,
|
|
2932
|
+
'a switch on its own read flag alone (switch on useLoadingScope() || isLoading: inside a scope it draws the loaded form with no data)',
|
|
2933
|
+
);
|
|
2934
|
+
return undefined;
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
/** Whether a condition reads a READ FLAG the component's own hook returns (not a context's, not the scope). */
|
|
2939
|
+
function ownReadFlag(test, fn, depth = 0) {
|
|
2940
|
+
let hit = false;
|
|
2941
|
+
const fromHook = init => {
|
|
2942
|
+
const call = strip(init);
|
|
2943
|
+
if (!ts.isCallExpression(call)) return false;
|
|
2944
|
+
const hook = dottedName(call.expression).split('.').pop();
|
|
2945
|
+
return (
|
|
2946
|
+
/^use[A-Z]/.test(hook) && !/Context$/.test(hook) && !project.scopeHooks.has(hook) && !LOCAL_HOOK.test(hook)
|
|
2947
|
+
);
|
|
2948
|
+
};
|
|
2949
|
+
const v = n => {
|
|
2950
|
+
if (hit || isFunctionLike(n)) return;
|
|
2951
|
+
const root = chainRoot(n);
|
|
2952
|
+
if ((ts.isIdentifier(n) || ts.isPropertyAccessExpression(n)) && root) {
|
|
2953
|
+
const decl = model.lookup(root.text, root);
|
|
2954
|
+
if (decl?.kind === 'var' && decl.init && nearestFunction(decl.node) === fn) {
|
|
2955
|
+
if (READ_FLAG.test(leafName(n)) && fromHook(decl.init)) hit = true;
|
|
2956
|
+
else if (ts.isIdentifier(n) && !decl.destructured && depth < 3 && ownReadFlag(decl.init, fn, depth + 1))
|
|
2957
|
+
hit = true;
|
|
2958
|
+
}
|
|
2959
|
+
if (hit) return;
|
|
2960
|
+
}
|
|
2961
|
+
if (ts.isBinaryExpression(n) && isLoadingFlag(n, model) && /===|==/.test(n.operatorToken.getText())) {
|
|
2962
|
+
const side = [n.left, n.right].map(strip).find(x => !ts.isStringLiteral(x));
|
|
2963
|
+
const sideRoot = side && chainRoot(side);
|
|
2964
|
+
const decl = sideRoot && model.lookup(sideRoot.text, sideRoot);
|
|
2965
|
+
if (decl?.kind === 'var' && decl.init && fromHook(decl.init)) hit = true;
|
|
2966
|
+
}
|
|
2967
|
+
ts.forEachChild(n, v);
|
|
2968
|
+
};
|
|
2969
|
+
v(test);
|
|
2970
|
+
return hit;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function isSkeletonOutput(node) {
|
|
2974
|
+
return !!skeletonOutput(node);
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
/** The skeleton element a branch draws (a `return`'s, or the branch itself), or null. */
|
|
2978
|
+
function skeletonOutput(branch) {
|
|
2979
|
+
let expr = branch;
|
|
2980
|
+
if (ts.isReturnStatement(branch)) expr = branch.expression;
|
|
2981
|
+
else if (ts.isBlock(branch)) {
|
|
2982
|
+
const ret = branch.statements.find(s => ts.isReturnStatement(s));
|
|
2983
|
+
expr = ret?.expression;
|
|
2984
|
+
}
|
|
2985
|
+
if (!expr) return null;
|
|
2986
|
+
const el = rootElement(expr);
|
|
2987
|
+
const opening = el && openingOf(el);
|
|
2988
|
+
if (!opening) return null;
|
|
2989
|
+
return isSkeletonName(model.resolveName(opening.tagName)) ? opening : null;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/** Whether a condition reads a boolean flag (a loading-like name, or a parameter typed `boolean`), not data presence. */
|
|
2993
|
+
function readsFlag(test, fn) {
|
|
2994
|
+
let hit = false;
|
|
2995
|
+
const v = n => {
|
|
2996
|
+
if (hit || isFunctionLike(n)) return;
|
|
2997
|
+
if ((ts.isIdentifier(n) || ts.isPropertyAccessExpression(n)) && SWITCH_FLAG.test(leafName(n))) hit = true;
|
|
2998
|
+
else if (ts.isIdentifier(n)) {
|
|
2999
|
+
const decl = model.lookup(n.text, n);
|
|
3000
|
+
if (decl?.kind === 'param' && declaredBoolean(decl.node, n.text)) hit = true;
|
|
3001
|
+
if (decl?.kind === 'var' && decl.init && nearestFunction(decl.node) === fn && readsFlag(decl.init, fn))
|
|
3002
|
+
hit = true;
|
|
3003
|
+
}
|
|
3004
|
+
if (!hit) ts.forEachChild(n, v);
|
|
3005
|
+
};
|
|
3006
|
+
v(test);
|
|
3007
|
+
return hit;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
/** Whether a parameter (or its destructured field `name`) is typed `boolean` in an inline type literal. */
|
|
3011
|
+
function declaredBoolean(param, name) {
|
|
3012
|
+
const type = param.type;
|
|
3013
|
+
if (!type) return false;
|
|
3014
|
+
if (ts.isIdentifier(param.name)) return type.kind === ts.SyntaxKind.BooleanKeyword;
|
|
3015
|
+
if (!ts.isTypeLiteralNode(type)) return false;
|
|
3016
|
+
return type.members.some(
|
|
3017
|
+
m => ts.isPropertySignature(m) && m.name.getText() === name && m.type?.kind === ts.SyntaxKind.BooleanKeyword,
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
/** Whether every identifier a condition reads comes from the component's parameters (or locals made from them). */
|
|
3022
|
+
function propDerived(test, fn, depth = 0) {
|
|
3023
|
+
let ids = 0;
|
|
3024
|
+
let ok = true;
|
|
3025
|
+
const v = n => {
|
|
3026
|
+
if (!ok) return;
|
|
3027
|
+
if (isFunctionLike(n)) return;
|
|
3028
|
+
if (ts.isPropertyAccessExpression(n)) {
|
|
3029
|
+
v(n.expression);
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
if (ts.isCallExpression(n)) {
|
|
3033
|
+
ok = false;
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
if (ts.isIdentifier(n)) {
|
|
3037
|
+
if (n.text === 'undefined') return;
|
|
3038
|
+
ids++;
|
|
3039
|
+
const decl = model.lookup(n.text, n);
|
|
3040
|
+
if (!decl) {
|
|
3041
|
+
ok = false;
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
if (decl.kind === 'param') {
|
|
3045
|
+
if (decl.fn !== fn) ok = false;
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
if (decl.kind === 'var' && decl.init && depth < 3 && nearestFunction(decl.node) === fn) {
|
|
3049
|
+
if (!propDerived(decl.init, fn, depth + 1)) ok = false;
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
ok = false;
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
ts.forEachChild(n, v);
|
|
3056
|
+
};
|
|
3057
|
+
v(test);
|
|
3058
|
+
return ok && ids > 0;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
// ---- page code (a host's route files and screens; see `CONFIGS.frontend.pageCode`) ---------------------------------
|
|
3062
|
+
if (page) checkPageCode(page);
|
|
3063
|
+
|
|
3064
|
+
function checkPageCode({ components: componentSkeletons }) {
|
|
3065
|
+
if (/(?:^|\/)loading\.tsx$/.test(rel) && rel.startsWith('app/'))
|
|
3066
|
+
report('PAGE_CODE', model.sf, 'a route-level loading.tsx (the page draws itself in a loading scope)');
|
|
3067
|
+
walk(model.sf, n => {
|
|
3068
|
+
const tag = renderedTag(n);
|
|
3069
|
+
if (tag) {
|
|
3070
|
+
const name = model.resolveName(tag);
|
|
3071
|
+
if (PRIMITIVE.test(name) || PRIMITIVE.test(dottedName(tag)))
|
|
3072
|
+
report('PAGE_CODE', n, `${name.split('.')[0]} (a skeleton primitive in page code)`);
|
|
3073
|
+
else if (componentSkeletons && isSkeletonName(name) && !inDynamicLoading(n))
|
|
3074
|
+
report('PAGE_CODE', n, `<${name}> rendered by page code (render the component: it draws its own skeleton)`);
|
|
3075
|
+
}
|
|
3076
|
+
if ((ts.isFunctionDeclaration(n) || ts.isVariableDeclaration(n)) && n.name && ts.isIdentifier(n.name)) {
|
|
3077
|
+
const fnLike =
|
|
3078
|
+
ts.isFunctionDeclaration(n) || (n.initializer && (functionInside(n.initializer) || isJsx(n.initializer)));
|
|
3079
|
+
if (fnLike && isComponentSkeletonName(n.name.text))
|
|
3080
|
+
report('PAGE_CODE', n.name, `${n.name.text} (a component declared as a skeleton)`);
|
|
3081
|
+
}
|
|
3082
|
+
return undefined;
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// ---- opt-outs: only the rule-scoped form, and only one that lifts something ---------------------------------------
|
|
3087
|
+
if (active.has('OPT_OUT')) {
|
|
3088
|
+
for (const marker of model.markers) {
|
|
3089
|
+
const problem = !marker.rule
|
|
3090
|
+
? 'an opt-out with no rule (write `// loading-standard-ok(RULE_ID): <why>`)'
|
|
3091
|
+
: !RULES[marker.rule] || marker.rule === 'OPT_OUT'
|
|
3092
|
+
? `an opt-out naming no rule of the engine (${marker.rule})`
|
|
3093
|
+
: !marker.why
|
|
3094
|
+
? `an opt-out of ${marker.rule} with no reason`
|
|
3095
|
+
: !active.has(marker.rule)
|
|
3096
|
+
? `an opt-out of ${marker.rule}, which does not run on this file (delete it)`
|
|
3097
|
+
: !marker.used
|
|
3098
|
+
? `an opt-out of ${marker.rule} that lifts nothing (delete it)`
|
|
3099
|
+
: null;
|
|
3100
|
+
if (problem) findings.push({ rule: 'OPT_OUT', path, file: rel, line: marker.line, message: problem });
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
return findings;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/** Whether `model`'s module renders `<component />` from a function that is not a skeleton. */
|
|
3108
|
+
function rendersOutsideSkeletons(model, component) {
|
|
3109
|
+
let hit = false;
|
|
3110
|
+
walk(model.sf, n => {
|
|
3111
|
+
if (hit) return false;
|
|
3112
|
+
const tag = renderedTag(n);
|
|
3113
|
+
if (tag && model.resolveName(tag) === component && !enclosingNames(n).some(isSkeletonFunctionName)) hit = true;
|
|
3114
|
+
return undefined;
|
|
3115
|
+
});
|
|
3116
|
+
return hit;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/** `path:line: message` lines, sorted by path then line. */
|
|
3120
|
+
export function formatFindings(findings, { separator = ': ' } = {}) {
|
|
3121
|
+
return [...findings]
|
|
3122
|
+
.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1))
|
|
3123
|
+
.map(f => `${f.file}:${f.line}${separator}${f.message} [${f.rule}]`);
|
|
3124
|
+
}
|