diff --git a/change/@fluentui-chart-web-components-9e64eade-4a6e-4ef5-804e-0302dbe8d0cf.json b/change/@fluentui-chart-web-components-9e64eade-4a6e-4ef5-804e-0302dbe8d0cf.json new file mode 100644 index 00000000000000..56696db327177c --- /dev/null +++ b/change/@fluentui-chart-web-components-9e64eade-4a6e-4ef5-804e-0302dbe8d0cf.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: fixing package bump issue to latest published version", + "packageName": "@fluentui/chart-web-components", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-web-components-2629fc8d-de98-44e1-aaaa-f6b149295390.json b/change/@fluentui-web-components-2629fc8d-de98-44e1-aaaa-f6b149295390.json new file mode 100644 index 00000000000000..4316355f198736 --- /dev/null +++ b/change/@fluentui-web-components-2629fc8d-de98-44e1-aaaa-f6b149295390.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: fixing package bump issue to latest published version", + "packageName": "@fluentui/web-components", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-web-components-9c7dc48c-180c-4d64-aeb6-b7e3f1415e45.json b/change/@fluentui-web-components-d5d5e166-edb4-4fc9-a778-a8a91ff5597b.json similarity index 63% rename from change/@fluentui-web-components-9c7dc48c-180c-4d64-aeb6-b7e3f1415e45.json rename to change/@fluentui-web-components-d5d5e166-edb4-4fc9-a778-a8a91ff5597b.json index 14d25fba181ab9..1745cafdc44e63 100644 --- a/change/@fluentui-web-components-9c7dc48c-180c-4d64-aeb6-b7e3f1415e45.json +++ b/change/@fluentui-web-components-d5d5e166-edb4-4fc9-a778-a8a91ff5597b.json @@ -1,6 +1,6 @@ { "type": "patch", - "comment": "fix focusgroup in tree when focusgroup is natively supported", + "comment": "remove using data-indent for styling", "packageName": "@fluentui/web-components", "email": "machi@microsoft.com", "dependentChangeType": "patch" diff --git a/packages/charts/chart-web-components/CHANGELOG.json b/packages/charts/chart-web-components/CHANGELOG.json index 86a5c74d8c8885..e7f59806abcc6f 100644 --- a/packages/charts/chart-web-components/CHANGELOG.json +++ b/packages/charts/chart-web-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@fluentui/chart-web-components", "entries": [ + { + "date": "Mon, 13 Jul 2026 04:09:41 GMT", + "tag": "@fluentui/chart-web-components_v0.0.92", + "version": "0.0.92", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@fluentui/chart-web-components", + "comment": "Bump @fluentui/web-components to v3.0.2", + "commit": "bc5035bfcd53277a39075c596b14b9731020d1de" + } + ] + } + }, { "date": "Tue, 07 Jul 2026 04:09:50 GMT", "tag": "@fluentui/chart-web-components_v0.0.91", diff --git a/packages/charts/chart-web-components/CHANGELOG.md b/packages/charts/chart-web-components/CHANGELOG.md index e2debed5c3ad6e..45bf9abf4433fe 100644 --- a/packages/charts/chart-web-components/CHANGELOG.md +++ b/packages/charts/chart-web-components/CHANGELOG.md @@ -1,9 +1,18 @@ # Change Log - @fluentui/chart-web-components -This log was last generated on Tue, 07 Jul 2026 04:09:50 GMT and should not be manually modified. +This log was last generated on Mon, 13 Jul 2026 04:09:41 GMT and should not be manually modified. +## [0.0.92](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.92) + +Mon, 13 Jul 2026 04:09:41 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/chart-web-components_v0.0.91..@fluentui/chart-web-components_v0.0.92) + +### Patches + +- Bump @fluentui/web-components to v3.0.2 ([PR #36379](https://github.com/microsoft/fluentui/pull/36379) by beachball) + ## [0.0.91](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.91) Tue, 07 Jul 2026 04:09:50 GMT diff --git a/packages/charts/chart-web-components/package.json b/packages/charts/chart-web-components/package.json index 4e8f9f82d55c3a..ed11cfb9a2eba3 100644 --- a/packages/charts/chart-web-components/package.json +++ b/packages/charts/chart-web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/chart-web-components", "description": "A library of Fluent Chart Web Components", - "version": "0.0.91", + "version": "0.0.92", "author": { "name": "Microsoft" }, @@ -68,7 +68,7 @@ }, "dependencies": { "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "^3.0.1", + "@fluentui/web-components": "^3.0.2", "@microsoft/fast-web-utilities": "^6.0.0", "@types/d3-selection": "^3.0.0", "@types/d3-shape": "^3.0.0", diff --git a/packages/web-components/CHANGELOG.json b/packages/web-components/CHANGELOG.json index 43fc3ce359e3a9..e1ee8ecfd120c7 100644 --- a/packages/web-components/CHANGELOG.json +++ b/packages/web-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@fluentui/web-components", "entries": [ + { + "date": "Mon, 13 Jul 2026 04:09:41 GMT", + "tag": "@fluentui/web-components_v3.0.2", + "version": "3.0.2", + "comments": { + "patch": [ + { + "author": "machi@microsoft.com", + "package": "@fluentui/web-components", + "commit": "bc5035bfcd53277a39075c596b14b9731020d1de", + "comment": "fix focusgroup in tree when focusgroup is natively supported" + } + ] + } + }, { "date": "Tue, 07 Jul 2026 04:09:49 GMT", "tag": "@fluentui/web-components_v3.0.1", diff --git a/packages/web-components/CHANGELOG.md b/packages/web-components/CHANGELOG.md index 3b255643397bb9..710eb0a358bd19 100644 --- a/packages/web-components/CHANGELOG.md +++ b/packages/web-components/CHANGELOG.md @@ -1,9 +1,18 @@ # Change Log - @fluentui/web-components -This log was last generated on Tue, 07 Jul 2026 04:09:49 GMT and should not be manually modified. +This log was last generated on Mon, 13 Jul 2026 04:09:41 GMT and should not be manually modified. +## [3.0.2](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.0.2) + +Mon, 13 Jul 2026 04:09:41 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/web-components_v3.0.1..@fluentui/web-components_v3.0.2) + +### Patches + +- fix focusgroup in tree when focusgroup is natively supported ([PR #36379](https://github.com/microsoft/fluentui/pull/36379) by machi@microsoft.com) + ## [3.0.1](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.0.1) Tue, 07 Jul 2026 04:09:49 GMT diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 4f545f4359c8ad..fb926bd4ada162 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/web-components", "description": "A library of Fluent Web Components", - "version": "3.0.1", + "version": "3.0.2", "author": { "name": "Microsoft", "url": "https://discord.gg/FcSNfg4" diff --git a/packages/web-components/src/accordion-item/accordion-item.styles.css b/packages/web-components/src/accordion-item/accordion-item.styles.css index f153a1bc1ce27a..77f0b448746392 100644 --- a/packages/web-components/src/accordion-item/accordion-item.styles.css +++ b/packages/web-components/src/accordion-item/accordion-item.styles.css @@ -8,6 +8,7 @@ :host { max-width: fit-content; contain: content; + color: var(--colorNeutralForeground1); } .heading { @@ -28,7 +29,7 @@ background: var(--colorTransparentBackground); border: none; box-sizing: border-box; - color: var(--colorNeutralForeground1); + color: inherit; cursor: pointer; font: inherit; grid-column: auto / span 2; @@ -138,6 +139,7 @@ button:focus-visible::after { :host([marker-position='end']) ::slotted([slot='start']) { grid-column: 1 / span 1; + color: currentColor; } :host([marker-position='end']) :is(.default-marker-collapsed, .default-marker-expanded) { diff --git a/packages/web-components/src/link/link.styles.css b/packages/web-components/src/link/link.styles.css index 3270cd6b093db0..a76a71b1fe8544 100644 --- a/packages/web-components/src/link/link.styles.css +++ b/packages/web-components/src/link/link.styles.css @@ -22,7 +22,7 @@ user-select: text; } -:host(:is(:hover, :focus-visible)) { +:host(:hover) { outline: none; text-decoration-line: underline; } @@ -55,6 +55,15 @@ text-decoration: underline; } +:host(:focus-visible), +:host-context(:is(h1, h2, h3, h4, h5, h6, p, fluent-text)):focus-visible, +:host([inline]:focus-visible) { + outline-style: none; + text-decoration-line: underline; + text-decoration-style: double; + text-decoration-color: var(--colorStrokeFocus2); +} + :host(:not([href])) { color: inherit; text-decoration: none; diff --git a/packages/web-components/src/menu-item/menu-item.styles.css b/packages/web-components/src/menu-item/menu-item.styles.css index 602161974824e3..09ea20584bd77f 100644 --- a/packages/web-components/src/menu-item/menu-item.styles.css +++ b/packages/web-components/src/menu-item/menu-item.styles.css @@ -2,25 +2,30 @@ display: none; } :host { - display: grid; + display: flex; } :host { - --indent: 0; align-items: center; background: var(--colorNeutralBackground1); border-radius: var(--borderRadiusMedium); + box-sizing: border-box; color: var(--colorNeutralForeground2); - contain: layout; cursor: pointer; - /* Prevent shrinking of MenuItems when max-height is applied to MenuList */ - flex-shrink: 0; font: var(--fontWeightRegular) var(--fontSizeBase300) / var(--lineHeightBase300) var(--fontFamilyBase); - grid-gap: 4px; - grid-template-columns: 20px 20px auto 20px; - height: 32px; + gap: 4px; + min-block-size: 32px; overflow: visible; - padding: 0 10px; + padding-inline: 10px; +} + +@supports (grid-template-columns: subgrid) { + :host { + display: grid; + gap: 0; + grid-template-columns: subgrid; + padding-inline: unset; + } } :host(:hover) { @@ -52,13 +57,6 @@ outline: 2px solid var(--colorStrokeFocus2); } -.content { - white-space: nowrap; - flex-grow: 1; - grid-column: auto / span 2; - padding: 0 2px; -} - :host(:not(:state(checked))) .indicator, :host(:not(:state(checked))) ::slotted([slot='indicator']), :host(:not(:state(submenu))) .submenu-glyph, @@ -72,46 +70,32 @@ white-space: nowrap; } -:host([data-indent='1']) { - --indent: 1; -} - -:host([data-indent='2']) { - --indent: 2; - grid-template-columns: 20px 20px auto auto; -} - -:host(:state(submenu)) { - grid-template-columns: 20px auto auto 20px; -} - -:host([data-indent='2']:state(submenu)) { - grid-template-columns: 20px 20px auto auto 20px; -} - .indicator, ::slotted([slot='indicator']) { - grid-column: 1 / span 1; + grid-column: 2 / span 1; width: 20px; } ::slotted([slot='start']) { display: inline-flex; - grid-column: calc(var(--indent)) / span 1; + grid-column: 3 / span 1; } .content { - grid-column: calc(var(--indent) + 1) / span 1; + flex-grow: 1; + grid-column: 4 / span 1; + padding: 0 2px; + white-space: nowrap; } ::slotted([slot='end']) { - grid-column: calc(var(--indent) + 2) / span 1; + grid-column: 5 / span 1; justify-self: end; } .submenu-glyph, ::slotted([slot='submenu-glyph']) { - grid-column: -2 / span 1; + grid-column: 6 / span 1; justify-self: end; } @@ -128,7 +112,7 @@ ::slotted([popover]) { margin: 0; - max-height: var(--menu-max-height, auto); + max-block-size: var(--menu-max-height, auto); position: fixed; position-anchor: --menu-trigger; inset: unset; diff --git a/packages/web-components/src/menu-item/menu-item.styles.ts b/packages/web-components/src/menu-item/menu-item.styles.ts index b28b47b94fe722..2514ea26d8873b 100644 --- a/packages/web-components/src/menu-item/menu-item.styles.ts +++ b/packages/web-components/src/menu-item/menu-item.styles.ts @@ -1,5 +1,5 @@ import { css } from '@microsoft/fast-element'; -import { display } from '../utils/display.js'; +import { checkedState, disabledState, submenuState } from '../styles/states/index.js'; import { borderRadiusMedium, colorCompoundBrandForeground1Pressed, @@ -20,30 +20,35 @@ import { lineHeightBase200, lineHeightBase300, } from '../theme/design-tokens.js'; -import { checkedState, disabledState, submenuState } from '../styles/states/index.js'; +import { display } from '../utils/display.js'; /** MenuItem styles * @public */ export const styles = css` - ${display('grid')} + ${display('flex')} :host { - --indent: 0; align-items: center; background: ${colorNeutralBackground1}; border-radius: ${borderRadiusMedium}; + box-sizing: border-box; color: ${colorNeutralForeground2}; - contain: layout; cursor: pointer; - /* Prevent shrinking of MenuItems when max-height is applied to MenuList */ - flex-shrink: 0; font: ${fontWeightRegular} ${fontSizeBase300} / ${lineHeightBase300} ${fontFamilyBase}; - grid-gap: 4px; - grid-template-columns: 20px 20px auto 20px; - height: 32px; + gap: 4px; + min-block-size: 32px; overflow: visible; - padding: 0 10px; + padding-inline: 10px; + } + + @supports (grid-template-columns: subgrid) { + :host { + display: grid; + gap: 0; + grid-template-columns: subgrid; + padding-inline: unset; + } } :host(:hover) { @@ -75,13 +80,6 @@ export const styles = css` outline: 2px solid ${colorStrokeFocus2}; } - .content { - white-space: nowrap; - flex-grow: 1; - grid-column: auto / span 2; - padding: 0 2px; - } - :host(:not(${checkedState})) .indicator, :host(:not(${checkedState})) ::slotted([slot='indicator']), :host(:not(${submenuState})) .submenu-glyph, @@ -95,46 +93,32 @@ export const styles = css` white-space: nowrap; } - :host([data-indent='1']) { - --indent: 1; - } - - :host([data-indent='2']) { - --indent: 2; - grid-template-columns: 20px 20px auto auto; - } - - :host(${submenuState}) { - grid-template-columns: 20px auto auto 20px; - } - - :host([data-indent='2']${submenuState}) { - grid-template-columns: 20px 20px auto auto 20px; - } - .indicator, ::slotted([slot='indicator']) { - grid-column: 1 / span 1; + grid-column: 2 / span 1; width: 20px; } ::slotted([slot='start']) { display: inline-flex; - grid-column: calc(var(--indent)) / span 1; + grid-column: 3 / span 1; } .content { - grid-column: calc(var(--indent) + 1) / span 1; + flex-grow: 1; + grid-column: 4 / span 1; + padding: 0 2px; + white-space: nowrap; } ::slotted([slot='end']) { - grid-column: calc(var(--indent) + 2) / span 1; + grid-column: 5 / span 1; justify-self: end; } .submenu-glyph, ::slotted([slot='submenu-glyph']) { - grid-column: -2 / span 1; + grid-column: 6 / span 1; justify-self: end; } @@ -151,7 +135,7 @@ export const styles = css` ::slotted([popover]) { margin: 0; - max-height: var(--menu-max-height, auto); + max-block-size: var(--menu-max-height, auto); position: fixed; position-anchor: --menu-trigger; inset: unset; diff --git a/packages/web-components/src/menu-list/menu-list.base.ts b/packages/web-components/src/menu-list/menu-list.base.ts index 01eb35094a0682..89f120627241fc 100644 --- a/packages/web-components/src/menu-list/menu-list.base.ts +++ b/packages/web-components/src/menu-list/menu-list.base.ts @@ -95,17 +95,6 @@ export class BaseMenuList extends FASTElement { this.menuItems?.find(item => !item.disabled)?.focus(); } - private static elementIndent(el: HTMLElement): MenuItemColumnCount { - const role = el.role; - const startSlot = el.querySelector('[slot=start]'); - - if (role && role !== MenuItemRole.menuitem) { - return startSlot ? 2 : 1; - } - - return startSlot ? 1 : 0; - } - protected setItems(): void { const children: HTMLElement[] = Array.from(this.children) as HTMLElement[]; children.forEach((child: Element) => { @@ -113,22 +102,7 @@ export class BaseMenuList extends FASTElement { }); this.menuChildren = children.filter(child => !child.hasAttribute('hidden')); - - /** - * Set the indent attribute on MenuItem elements based on their - * position in the MenuList. Each MenuItem element has a data-indent attribute that is - * used to set the indent of the element's start slot content. - */ this.menuItems = this.menuChildren?.filter(this.isMenuItemElement); - const indent: MenuItemColumnCount = this.menuItems?.reduce((accum, current) => { - const elementValue = BaseMenuList.elementIndent(current as HTMLElement); - - return Math.max(accum, elementValue as number) as MenuItemColumnCount; - }, 0); - - this.menuItems?.forEach((item: HTMLElement) => { - item.dataset.indent = `${indent}`; - }); } /** diff --git a/packages/web-components/src/menu-list/menu-list.spec.ts b/packages/web-components/src/menu-list/menu-list.spec.ts index 125a7a1cceae26..45da8a6ee4e494 100644 --- a/packages/web-components/src/menu-list/menu-list.spec.ts +++ b/packages/web-components/src/menu-list/menu-list.spec.ts @@ -444,197 +444,6 @@ test.describe('MenuList', () => { await expect(menuItems.nth(1)).toBeFocused(); }); - test('should set the data-indent attribute to 0 correctly on all MenuItem elements when role of menuitem and not content in start slot', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate(); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '0'); - } - }); - - test('should set the data-indent attribute to 1 correctly on all MenuItem elements when a menuitem in the menu as a role of menuitemcheckbox', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ - innerHTML: /* html */ ` - <${MenuItemTagName} role="menuitemcheckbox"> - <${MenuItemTagName}>Menu item 2 - <${MenuItemTagName}>Menu item 3 - <${MenuItemTagName}>Menu item 4 - `, - }); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '1'); - } - }); - - test('should set the data-indent attribute to 1 correctly on all MenuItem elements when a menuitem in the menu as a role of menuitemradio', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ - innerHTML: /* html */ ` - <${MenuItemTagName} role="menuitemradio"> - <${MenuItemTagName}>Menu item 2 - <${MenuItemTagName}>Menu item 3 - <${MenuItemTagName}>Menu item 4 - `, - }); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '1'); - } - }); - - test('should set the data-indent attribute to 2 correctly on all MenuItem elements when a menuitem in the menu has a role of menuitemcheckbox and content in the start slot', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ - innerHTML: /* html */ ` - <${MenuItemTagName} role="menuitemcheckbox"> - Item 1 - Icon - - <${MenuItemTagName}>Menu item 2 - <${MenuItemTagName}>Menu item 3 - <${MenuItemTagName}>Menu item 4 - `, - }); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '2'); - } - }); - - test('should set the data-indent attribute to 2 correctly on all MenuItem elements when a menuitem in the menu has a role of menuitemradio and content in the start slot', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ - innerHTML: /* html */ ` - <${MenuItemTagName} role="menuitemradio"> Item 1 Icon - <${MenuItemTagName}>Menu item 2 - <${MenuItemTagName}>Menu item 3 - <${MenuItemTagName}>Menu item 4 - `, - }); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '2'); - } - }); - - test('should set the data-indent attribute correctly when menu items are dynamically appended', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ innerHTML: '' }); - - await element.evaluate((node, MenuItemTagName) => { - const items = ['item 1', 'item 2', 'item 3']; - - items.forEach(item => { - const menuItem = document.createElement(MenuItemTagName); - menuItem.role = 'menuitemradio'; - menuItem.textContent = item; - node.append(menuItem); - }); - }, MenuItemTagName); - - await expect(menuItems).toHaveCount(3); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '1'); - } - }); - - test('should set the data-indent attribute correctly when menu items are appended via a DocumentFragment', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate({ innerHTML: '' }); - - await element.evaluate((node, MenuItemTagName) => { - const fragment = document.createDocumentFragment(); - const items = ['item 1', 'item 2', 'item 3']; - - items.forEach(item => { - const menuItem = document.createElement(MenuItemTagName); - menuItem.role = 'menuitemradio'; - menuItem.textContent = item; - fragment.append(menuItem); - }); - - node.append(fragment); - }, MenuItemTagName); - - await expect(menuItems).toHaveCount(3); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '1'); - } - }); - - test('should update data-indent on existing items when a menuitemradio is appended and removed', async ({ - fastPage, - }) => { - const { element } = fastPage; - const menuItems = element.locator(MenuItemTagName); - - await fastPage.setTemplate(); - - await test.step('all plain menuitems should start with data-indent 0', async () => { - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '0'); - } - }); - - await test.step('appending a menuitemradio should update all items to data-indent 1', async () => { - await element.evaluate((node, MenuItemTagName) => { - const menuItem = document.createElement(MenuItemTagName); - menuItem.role = 'menuitemradio'; - menuItem.textContent = 'Radio item'; - node.append(menuItem); - }, MenuItemTagName); - - await expect(menuItems).toHaveCount(5); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '1'); - } - }); - - await test.step('removing the menuitemradio should revert all items to data-indent 0', async () => { - await menuItems.last().evaluate(node => node.remove()); - - await expect(menuItems).toHaveCount(4); - - for (const item of await menuItems.all()) { - await expect(item).toHaveAttribute('data-indent', '0'); - } - }); - }); - test.describe('`change` event', () => { test('should emit `change` event when `checked` property changed', async ({ fastPage }) => { const { element } = fastPage; diff --git a/packages/web-components/src/menu-list/menu-list.styles.css b/packages/web-components/src/menu-list/menu-list.styles.css index f3fe401c6df15d..076c3ee0f1109d 100644 --- a/packages/web-components/src/menu-list/menu-list.styles.css +++ b/packages/web-components/src/menu-list/menu-list.styles.css @@ -2,19 +2,71 @@ display: none; } :host { - display: flex; + display: grid; } :host { - flex-direction: column; - height: fit-content; - max-width: 300px; - min-width: 160px; - width: auto; + --_col-indicator-width: 0px; + --_col-start-width: 0px; + --_col-content-width: 1fr; + --_col-end-width: 0px; + --_col-submenu-width: 0px; + + contain: content; + block-size: fit-content; + max-inline-size: 300px; + min-inline-size: 160px; + inline-size: auto; background-color: var(--colorNeutralBackground1); border: 1px solid var(--colorTransparentStroke); border-radius: var(--borderRadiusMedium); box-shadow: var(--shadow16); + grid-template-columns: + 10px + var(--_col-indicator-width) + var(--_col-start-width) + var(--_col-content-width) + var(--_col-end-width) + var(--_col-submenu-width) + 10px; + gap: 2px 0; padding: 4px; - row-gap: 2px; +} + +::slotted(*) { + grid-column: 1 / -1; +} + +:host(:has([role='menuitemradio'], [role='menuitemcheckbox'])) { + --_col-indicator-width: 24px; +} + +:host(:has([slot='start'])) { + --_col-start-width: 24px; +} + +:host(:has([slot='end'])) { + --_col-end-width: 24px; +} + +:host(:has([slot='submenu'])) { + --_col-submenu-width: 24px; +} + +@scope { + :scope:has([role='menuitemradio'], [role='menuitemcheckbox']) { + --_col-indicator-width: 24px; + } + + :scope:has([slot='start']) { + --_col-start-width: 24px; + } + + :scope:has([slot='end']) { + --_col-end-width: 24px; + } + + :scope:has([slot='submenu']) { + --_col-submenu-width: 24px; + } } diff --git a/packages/web-components/src/menu-list/menu-list.styles.ts b/packages/web-components/src/menu-list/menu-list.styles.ts index 5931497619f9bb..7cd59519ccddd2 100644 --- a/packages/web-components/src/menu-list/menu-list.styles.ts +++ b/packages/web-components/src/menu-list/menu-list.styles.ts @@ -1,29 +1,81 @@ import { css } from '@microsoft/fast-element'; -import { display } from '../utils/display.js'; import { borderRadiusMedium, colorNeutralBackground1, colorTransparentStroke, shadow16, } from '../theme/design-tokens.js'; +import { display } from '../utils/display.js'; /** MenuList styles * @public */ export const styles = css` - ${display('flex')} + ${display('grid')} :host { - flex-direction: column; - height: fit-content; - max-width: 300px; - min-width: 160px; - width: auto; + --_col-indicator-width: 0px; + --_col-start-width: 0px; + --_col-content-width: 1fr; + --_col-end-width: 0px; + --_col-submenu-width: 0px; + + contain: content; + block-size: fit-content; + max-inline-size: 300px; + min-inline-size: 160px; + inline-size: auto; background-color: ${colorNeutralBackground1}; border: 1px solid ${colorTransparentStroke}; border-radius: ${borderRadiusMedium}; box-shadow: ${shadow16}; + grid-template-columns: + 10px + var(--_col-indicator-width) + var(--_col-start-width) + var(--_col-content-width) + var(--_col-end-width) + var(--_col-submenu-width) + 10px; + gap: 2px 0; padding: 4px; - row-gap: 2px; + } + + ::slotted(*) { + grid-column: 1 / -1; + } + + :host(:has([role='menuitemradio'], [role='menuitemcheckbox'])) { + --_col-indicator-width: 24px; + } + + :host(:has([slot='start'])) { + --_col-start-width: 24px; + } + + :host(:has([slot='end'])) { + --_col-end-width: 24px; + } + + :host(:has([slot='submenu'])) { + --_col-submenu-width: 24px; + } + + @scope { + :scope:has([role='menuitemradio'], [role='menuitemcheckbox']) { + --_col-indicator-width: 24px; + } + + :scope:has([slot='start']) { + --_col-start-width: 24px; + } + + :scope:has([slot='end']) { + --_col-end-width: 24px; + } + + :scope:has([slot='submenu']) { + --_col-submenu-width: 24px; + } } `; diff --git a/packages/web-components/src/message-bar/message-bar.styles.css b/packages/web-components/src/message-bar/message-bar.styles.css index 0396ff80154197..de746916da337f 100644 --- a/packages/web-components/src/message-bar/message-bar.styles.css +++ b/packages/web-components/src/message-bar/message-bar.styles.css @@ -6,6 +6,7 @@ line-height: var(--lineHeightBase200); width: 100%; background: var(--colorNeutralBackground3); + color: var(--colorNeutralForeground3); border: 1px solid var(--colorNeutralStroke1); padding-inline: var(--spacingHorizontalM); border-radius: var(--borderRadiusMedium); diff --git a/packages/web-components/src/tab/tab.styles.css b/packages/web-components/src/tab/tab.styles.css index a22fc5f5173a18..1f54160549f71b 100644 --- a/packages/web-components/src/tab/tab.styles.css +++ b/packages/web-components/src/tab/tab.styles.css @@ -22,10 +22,12 @@ border-radius: var(--borderRadiusMedium); gap: 4px; } + :host .tab-content { display: inline-flex; flex-direction: column; padding: 0 2px; + grid-column: 3; } :host([aria-selected='true']) { @@ -81,6 +83,14 @@ background-color: var(--colorNeutralForegroundDisabled); } +::slotted([slot='start']) { + grid-column: 2; +} + +::slotted([slot='end']) { + grid-column: -1; +} + ::slotted([slot='start']), ::slotted([slot='end']) { display: flex; @@ -106,15 +116,6 @@ outline: 1px solid var(--colorStrokeFocus1); } -:host([data-hasIndent]) { - display: grid; - grid-template-columns: 20px 1fr auto; -} - -:host([data-hasIndent]) .tab-content { - grid-column: 2; -} - @media (forced-colors: active) { :host([aria-selected='true'])::after { background-color: Highlight; diff --git a/packages/web-components/src/tab/tab.styles.ts b/packages/web-components/src/tab/tab.styles.ts index d3a4c766b9f7c7..2f94a1bdfac0a8 100644 --- a/packages/web-components/src/tab/tab.styles.ts +++ b/packages/web-components/src/tab/tab.styles.ts @@ -39,10 +39,12 @@ export const styles = css` border-radius: ${borderRadiusMedium}; gap: 4px; } + :host .tab-content { display: inline-flex; flex-direction: column; padding: 0 2px; + grid-column: 3; } :host([aria-selected='true']) { @@ -98,6 +100,14 @@ export const styles = css` background-color: ${colorNeutralForegroundDisabled}; } + ::slotted([slot='start']) { + grid-column: 2; + } + + ::slotted([slot='end']) { + grid-column: -1; + } + ::slotted([slot='start']), ::slotted([slot='end']) { display: flex; @@ -123,15 +133,6 @@ export const styles = css` outline: 1px solid ${colorStrokeFocus1}; } - :host([data-hasIndent]) { - display: grid; - grid-template-columns: 20px 1fr auto; - } - - :host([data-hasIndent]) .tab-content { - grid-column: 2; - } - @media (forced-colors: active) { :host([aria-selected='true'])::after { background-color: Highlight; diff --git a/packages/web-components/src/tablist/tablist.base.ts b/packages/web-components/src/tablist/tablist.base.ts index bb7ee0ad365e6d..a009fbff49e005 100644 --- a/packages/web-components/src/tablist/tablist.base.ts +++ b/packages/web-components/src/tablist/tablist.base.ts @@ -109,7 +109,6 @@ export class BaseTablist extends FASTElement { return; } - const hasStartSlot = this.tabs.some(tab => !!tab.querySelector("[slot='start']")); const rootNode = this.getRootNode() as Document | ShadowRoot; let firstEnabledTabId = ''; @@ -133,9 +132,6 @@ export class BaseTablist extends FASTElement { tab.toggleAttribute('focusgroupstart', isSelected); tab.setAttribute('aria-selected', isSelected.toString()); - // Only set the data-hasIndent attribute if the tab has a start slot and the orientation is vertical - tab.toggleAttribute('data-hasIndent', hasStartSlot && this.orientation === TablistOrientation.vertical); - if (connectToPanel) { const ariaControls = tab.getAttribute('aria-controls') ?? ''; const panel = rootNode.getElementById(ariaControls); diff --git a/packages/web-components/src/tablist/tablist.spec.ts b/packages/web-components/src/tablist/tablist.spec.ts index 4d95f4f0bd83ac..d40c2aece1aeea 100644 --- a/packages/web-components/src/tablist/tablist.spec.ts +++ b/packages/web-components/src/tablist/tablist.spec.ts @@ -485,21 +485,4 @@ test.describe('Tablist', () => { await expect(secondPanel).toBeVisible(); await expect(thirdPanel).toBeHidden(); }); - - test('should set data-hasIndent on all tabs when any tab has a start slot', async ({ fastPage }) => { - const { element } = fastPage; - await fastPage.setTemplate({ - attributes: { orientation: 'vertical' }, - innerHTML: /* html */ ` - <${TabTagName}>Tab one - <${TabTagName}>TTab two - <${TabTagName}>Tab three - `, - }); - const tabs = element.locator(TabTagName); - - await expect(tabs.nth(0)).toHaveAttribute('data-hasIndent'); - await expect(tabs.nth(1)).toHaveAttribute('data-hasIndent'); - await expect(tabs.nth(2)).toHaveAttribute('data-hasIndent'); - }); }); diff --git a/packages/web-components/src/tablist/tablist.styles.css b/packages/web-components/src/tablist/tablist.styles.css index 71072de4988047..042206d37b1c16 100644 --- a/packages/web-components/src/tablist/tablist.styles.css +++ b/packages/web-components/src/tablist/tablist.styles.css @@ -29,7 +29,19 @@ :host([orientation='vertical']) { --tabPaddingBlock: var(--spacingVerticalS); --tabIndicatorInsetBlock: var(--spacingVerticalS); - flex-direction: column; + --_col-start-width: 0px; + display: grid; + grid-template-columns: var(--spacingHorizontalMNudge) var(--_col-start-width) 1fr auto var(--spacingHorizontalMNudge); +} + +:host(:has([slot='start'])) { + --_col-start-width: 24px; +} + +@scope { + :scope:has([slot='start']) { + --_col-start-width: 24px; + } } :host([orientation='vertical'][size='small']) { @@ -49,6 +61,12 @@ :host([orientation='vertical']) ::slotted([role='tab']) { justify-content: flex-start; + display: grid; + gap: 0; + grid-column: 1 / -1; + grid-template-columns: subgrid; + grid-row: unset; + padding-inline: 0; } :host ::slotted([slot='tab'])::after { @@ -160,7 +178,7 @@ * TODO: Remove '(text-size-adjust: auto)' after this bug is fixed: * https://bugs.webkit.org/show_bug.cgi?id=298646 * Also remove the same trick from tab.styles.ts. - * Using '@supports (text-size-adjust: auto)' here to exclude Safari 26 from + * Using '@supports (text-size-adjust: auto)' here to exclude Safari 26.0 from * using CSS Anchor Positioning here because it crashes. */ @supports (anchor-name: --a) and (text-size-adjust: auto) { @@ -172,7 +190,8 @@ background-color: var(--colorCompoundBrandStroke); content: ''; inline-size: 100%; - inset: auto auto anchor(end) anchor(center); + inset-block: auto anchor(end); + inset-inline: anchor(center) auto; position: fixed; position-anchor: --tab; transform: translateX(-50%); @@ -187,8 +206,13 @@ height: var(--strokeWidthThicker); } + :host(:dir(rtl))::after { + transform: translateX(50%); + } + :host([orientation='vertical'])::after { - inset: anchor(center) anchor(end) auto 0; + inset-block: anchor(center) auto; + inset-inline: anchor(start) auto; transform: translateY(-50%); transition-property: inset-block, height; @@ -197,10 +221,6 @@ height: calc(anchor-size() - var(--tabIndicatorInsetBlock) * 2); } - :host(:dir(rtl)[orientation='vertical'])::after { - inset: anchor(center) anchor(start) auto 0; - } - :host([disabled])::after { background-color: var(--colorNeutralForegroundDisabled); } diff --git a/packages/web-components/src/tablist/tablist.styles.ts b/packages/web-components/src/tablist/tablist.styles.ts index 77e217db58600c..d39c30dfb3ebfc 100644 --- a/packages/web-components/src/tablist/tablist.styles.ts +++ b/packages/web-components/src/tablist/tablist.styles.ts @@ -57,7 +57,19 @@ export const styles = css` :host([orientation='vertical']) { --tabPaddingBlock: ${spacingVerticalS}; --tabIndicatorInsetBlock: ${spacingVerticalS}; - flex-direction: column; + --_col-start-width: 0px; + display: grid; + grid-template-columns: ${spacingHorizontalMNudge} var(--_col-start-width) 1fr auto ${spacingHorizontalMNudge}; + } + + :host(:has([slot='start'])) { + --_col-start-width: 24px; + } + + @scope { + :scope:has([slot='start']) { + --_col-start-width: 24px; + } } :host([orientation='vertical'][size='small']) { @@ -77,6 +89,12 @@ export const styles = css` :host([orientation='vertical']) ::slotted([role='tab']) { justify-content: flex-start; + display: grid; + gap: 0; + grid-column: 1 / -1; + grid-template-columns: subgrid; + grid-row: unset; + padding-inline: 0; } :host ::slotted([slot='tab'])::after { diff --git a/packages/web-components/src/tree-item/tree-item.base.ts b/packages/web-components/src/tree-item/tree-item.base.ts index b349a367db055c..c05f7b72d99cc1 100644 --- a/packages/web-components/src/tree-item/tree-item.base.ts +++ b/packages/web-components/src/tree-item/tree-item.base.ts @@ -133,30 +133,6 @@ export class BaseTreeItem extends FASTElement { @attr({ mode: 'boolean' }) public empty: boolean = false; - private styles: ElementStyles | undefined; - - /** - * The indent of the tree item element. - * This is not needed once css attr() is supported (--indent: attr(data-indent type())); - * @public - */ - @attr({ attribute: 'data-indent' }) - public dataIndent!: number | undefined; - - protected dataIndentChanged(prev: number, next: number) { - if (this.styles !== undefined) { - this.$fastController.removeStyles(this.styles); - } - - this.styles = css` - :host { - --indent: ${next as any}; - } - `; - - this.$fastController.addStyles(this.styles); - } - /** @internal */ @observable public childTreeItems: BaseTreeItem[] | undefined = []; @@ -172,7 +148,7 @@ export class BaseTreeItem extends FASTElement { } /** - * Updates the childrens indent + * Updates the children’s selected states. * * @public */ @@ -185,18 +161,6 @@ export class BaseTreeItem extends FASTElement { if (!this.expanded) { this.expanded = Array.from(this.querySelectorAll('[selected]')).some(el => isTreeItem(el)); } - - this.childTreeItems.forEach(item => { - this.setIndent(item); - }); - } - - /** - * Sets the indent for each item - */ - private setIndent(item: BaseTreeItem): void { - const indent = this.dataIndent ?? 0; - item.dataIndent = indent + 1; } /** diff --git a/packages/web-components/src/tree-item/tree-item.styles.css b/packages/web-components/src/tree-item/tree-item.styles.css index e1889226efb1d3..48e3d39c5ae420 100644 --- a/packages/web-components/src/tree-item/tree-item.styles.css +++ b/packages/web-components/src/tree-item/tree-item.styles.css @@ -6,11 +6,17 @@ } :host { + --_subitem-padding-inline-start: var(--spacingHorizontalXXL); + outline: none; font-size: var(--fontSizeBase300); line-height: var(--lineHeightBase300); } +:host([size='small']) { + --_subitem-padding-inline-start: var(--spacingHorizontalM); +} + :host(:focus-visible) .positioning-region { box-shadow: var(--spacingVerticalNone) var(--spacingVerticalNone) var(--spacingVerticalNone) var(--spacingVerticalXXS) var(--colorStrokeFocus2) inset; @@ -27,7 +33,6 @@ justify-content: space-between; cursor: pointer; height: var(--spacingVerticalXXXL); - padding-inline-start: calc(var(--indent) * var(--spacingHorizontalXXL)); padding-inline-end: var(--spacingVerticalS); border-radius: var(--borderRadiusMedium); background-color: var(--colorSubtleBackground); @@ -100,6 +105,7 @@ .items { display: none; + padding-inline-start: var(--_subitem-padding-inline-start); } :host([expanded]) .items { @@ -123,7 +129,7 @@ :host([size='small']) .positioning-region { height: var(--spacingVerticalXXL); - padding-inline-start: calc(var(--indent) * var(--spacingHorizontalM)); + padding-inline-start: var(--spacingHorizontalM); } :host([appearance='subtle-alpha']) .positioning-region:hover { diff --git a/packages/web-components/src/tree-item/tree-item.styles.ts b/packages/web-components/src/tree-item/tree-item.styles.ts index 5a2c513e506232..a3b3a9acef3896 100644 --- a/packages/web-components/src/tree-item/tree-item.styles.ts +++ b/packages/web-components/src/tree-item/tree-item.styles.ts @@ -38,11 +38,17 @@ export const styles = css` ${display('block')} :host { + --_subitem-padding-inline-start: ${spacingHorizontalXXL}; + outline: none; font-size: ${fontSizeBase300}; line-height: ${lineHeightBase300}; } + :host([size='small']) { + --_subitem-padding-inline-start: ${spacingHorizontalM}; + } + :host(:focus-visible) .positioning-region { box-shadow: ${spacingVerticalNone} ${spacingVerticalNone} ${spacingVerticalNone} ${spacingVerticalXXS} ${colorStrokeFocus2} inset; @@ -59,7 +65,6 @@ export const styles = css` justify-content: space-between; cursor: pointer; height: ${spacingVerticalXXXL}; - padding-inline-start: calc(var(--indent) * ${spacingHorizontalXXL}); padding-inline-end: ${spacingVerticalS}; border-radius: ${borderRadiusMedium}; background-color: ${colorSubtleBackground}; @@ -132,6 +137,7 @@ export const styles = css` .items { display: none; + padding-inline-start: var(--_subitem-padding-inline-start); } :host([expanded]) .items { @@ -155,7 +161,7 @@ export const styles = css` :host([size='small']) .positioning-region { height: ${spacingVerticalXXL}; - padding-inline-start: calc(var(--indent) * ${spacingHorizontalM}); + padding-inline-start: ${spacingHorizontalM}; } :host([appearance='subtle-alpha']) .positioning-region:hover { diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/.swcrc b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/.swcrc new file mode 100644 index 00000000000000..d1620783f68e72 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/.swcrc @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "exclude": [ + "/testing", + "/**/*.cy.ts", + "/**/*.cy.tsx", + "/**/*.spec.ts", + "/**/*.spec.tsx", + "/**/*.test.ts", + "/**/*.test.tsx" + ], + "jsc": { + "baseUrl": ".", + "parser": { + "syntax": "typescript", + "tsx": false, + "decorators": false, + "dynamicImport": false + }, + "externalHelpers": true, + "target": "es2019" + }, + "minify": false, + "sourceMaps": true +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/config/api-extractor.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/config/api-extractor.json new file mode 100644 index 00000000000000..1ef0a6d4154a5a --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/config/api-extractor.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/dist/out-tsc/index.d.ts", + "docModel": { + "enabled": false + }, + "apiReport": { + "enabled": true + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/index.d.ts" + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "extractorMessageReporting": { + "ae-forgotten-export": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + } + } + }, + "newlineKind": "os" +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/etc/esm-first-proj.api.md b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/etc/esm-first-proj.api.md new file mode 100644 index 00000000000000..b140fb4f5e8d70 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/etc/esm-first-proj.api.md @@ -0,0 +1,12 @@ +## API Report File for "esm-first-proj" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export function greeter(name: string): string; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/package.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/package.json new file mode 100644 index 00000000000000..be229afe72e680 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/package.json @@ -0,0 +1,3 @@ +{ + "name": "esm-first-proj" +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/greeter.ts b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/greeter.ts new file mode 100644 index 00000000000000..8f81473d533128 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/greeter.ts @@ -0,0 +1,5 @@ +import { shout } from './helper'; + +export function greeter(name: string): string { + return shout(`hello ${name}`); +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/helper.ts b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/helper.ts new file mode 100644 index 00000000000000..ee33c7a86f594d --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/helper.ts @@ -0,0 +1,3 @@ +export function shout(word: string): string { + return word.toUpperCase(); +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/index.ts b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/index.ts new file mode 100644 index 00000000000000..f1f87dc2526fe8 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/src/index.ts @@ -0,0 +1 @@ +export { greeter } from './greeter'; diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.json new file mode 100644 index 00000000000000..25052e8384499b --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ES2019", + "skipLibCheck": true, + "pretty": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.lib.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.lib.json new file mode 100644 index 00000000000000..7b3b02933dd526 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/esm-first-proj/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["src/*.ts"], + "exclude": ["src/*.spec.ts"] +} diff --git a/tools/workspace-plugin/src/executors/build/executor.spec.ts b/tools/workspace-plugin/src/executors/build/executor.spec.ts index e43d815289d8aa..0b32b2199f5e2b 100644 --- a/tools/workspace-plugin/src/executors/build/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/build/executor.spec.ts @@ -390,6 +390,91 @@ describe('Build Executor', () => { }, 60000); }); + describe(`#__esmFirst`, () => { + const esmFirstContext: ExecutorContext = { + root: workspaceRoot, + cwd: process.cwd(), + isVerbose: false, + projectName: 'esm-first-proj', + projectsConfigurations: { + version: 2, + projects: { + 'esm-first-proj': { + root: 'libs/esm-first-proj', + name: 'esm-first-proj', + }, + }, + }, + nxJsonConfiguration: {}, + projectGraph: { nodes: {}, dependencies: {} }, + }; + + it('renames lib-commonjs output to *.cjs, rewrites relative require() extensions, and copies dist/*.d.cts', async () => { + const esmFirstOptions: BuildExecutorSchema = { + sourceRoot: 'src', + outputPathRoot: 'libs/esm-first-proj/dist', + moduleOutput: [ + { module: 'es6', outputPath: 'lib' }, + { module: 'commonjs', outputPath: 'lib-commonjs' }, + ], + assets: [], + generateApi: true, + clean: true, + __esmFirst: true, + }; + + const output = await executor(esmFirstOptions, esmFirstContext); + expect(output.success).toBe(true); + + // ESM (`lib`) output keeps *.js, with fully-resolved relative specifiers (resolveFully, driven by .swcrc's jsc.baseUrl) + expect(readdirSync(join(workspaceRoot, 'libs/esm-first-proj/lib')).sort()).toEqual([ + 'greeter.js', + 'greeter.js.map', + 'helper.js', + 'helper.js.map', + 'index.js', + 'index.js.map', + ]); + const greeterEsm = readFileSync(join(workspaceRoot, 'libs/esm-first-proj/lib/greeter.js'), 'utf-8'); + expect(greeterEsm).toContain(`./helper.js`); + + // commonjs output is renamed to *.cjs and its relative `require()` is rewritten to the `.cjs` extension + expect(readdirSync(join(workspaceRoot, 'libs/esm-first-proj/lib-commonjs')).sort()).toEqual([ + 'greeter.cjs', + 'greeter.cjs.map', + 'helper.cjs', + 'helper.cjs.map', + 'index.cjs', + 'index.cjs.map', + ]); + const greeterCjs = readFileSync(join(workspaceRoot, 'libs/esm-first-proj/lib-commonjs/greeter.cjs'), 'utf-8'); + expect(greeterCjs).toContain(`require("./helper.cjs")`); + expect(greeterCjs).not.toContain(`./helper.js`); + + // package.json itself is never modified by this temporary flag + expect(readFileSync(join(workspaceRoot, 'libs/esm-first-proj/package.json'), 'utf-8')).toBe( + '{\n "name": "esm-first-proj"\n}\n', + ); + + // `copyCjsTypes` is driven by the same `isEsmPackage` flag as the `.cjs` rename - unified via `options.isEsmPackage` + expect(existsSync(join(workspaceRoot, 'libs/esm-first-proj/dist/index.d.cts'))).toBe(true); + expect(readFileSync(join(workspaceRoot, 'libs/esm-first-proj/dist/index.d.cts'), 'utf-8')).toBe( + readFileSync(join(workspaceRoot, 'libs/esm-first-proj/dist/index.d.ts'), 'utf-8'), + ); + }, 60000); + + it('fails fast when ESM-first postprocessing is enabled but .swcrc is missing "jsc.baseUrl"', async () => { + // `libs/proj/.swcrc` has no `jsc.baseUrl`, so its commonjs output never gets extensioned relative + // `require()` specifiers - renaming those files to `.cjs` would ship an unrequireable package. + const optionsWithoutBaseUrl: BuildExecutorSchema = { + ...options, + __esmFirst: true, + }; + + await expect(executor(optionsWithoutBaseUrl, context)).rejects.toThrow(/jsc\.baseUrl/); + }); + }); + describe(`#reactCompiler`, () => { const reactCompilerContext: ExecutorContext = { root: workspaceRoot, diff --git a/tools/workspace-plugin/src/executors/build/executor.ts b/tools/workspace-plugin/src/executors/build/executor.ts index fddb58be2f1467..55aba5910de1e2 100644 --- a/tools/workspace-plugin/src/executors/build/executor.ts +++ b/tools/workspace-plugin/src/executors/build/executor.ts @@ -4,6 +4,7 @@ import { compileSwc } from './lib/swc'; import { compileWithGriffelStylesAOT, compileWithReactCompiler, hasStylesFilesToProcess } from './lib/babel'; import { assetGlobsToFiles, copyAssets } from './lib/assets'; import { cleanOutput } from './lib/clean'; +import { cjsRenameTransforms, copyCjsTypes } from './lib/cjs-extension'; import { NormalizedOptions, normalizeOptions, processAsyncQueue, runInParallel, runSerially } from './lib/shared'; import { measureEnd, measureStart } from '../../utils'; @@ -33,6 +34,7 @@ const runExecutor: PromiseExecutor = async (schema, context }, ), () => copyAssets(assetFiles), + () => copyCjsTypes(options), ); measureEnd('BuildExecutor'); @@ -54,7 +56,7 @@ async function runBuild(options: NormalizedOptions, _context: ExecutorContext): } const compilationQueue = options.moduleOutput.map(outputConfig => { - return compileSwc(outputConfig, options); + return compileSwc(outputConfig, options, cjsRenameTransforms(outputConfig, options)); }); return processAsyncQueue(compilationQueue); diff --git a/tools/workspace-plugin/src/executors/build/lib/babel.ts b/tools/workspace-plugin/src/executors/build/lib/babel.ts index ed61bbf8234eb1..3770e15166bdb6 100644 --- a/tools/workspace-plugin/src/executors/build/lib/babel.ts +++ b/tools/workspace-plugin/src/executors/build/lib/babel.ts @@ -14,6 +14,7 @@ import { type Config } from '@swc/core'; import { processAsyncQueue, type NormalizedOptions } from './shared'; import { compileSwc } from './swc'; +import { cjsRenameTransforms } from './cjs-extension'; const EOL_REGEX = /\r?\n/g; @@ -33,7 +34,7 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions) { if (!esmConfig) { logger.warn('es6 module output not specified. Skipping griffel AOT...'); const compilationQueue = restOfConfigs.map(outputConfig => { - return compileSwc(outputConfig, options); + return compileSwc(outputConfig, options, cjsRenameTransforms(outputConfig, options)); }); return processAsyncQueue(compilationQueue); } @@ -66,10 +67,11 @@ export async function compileWithGriffelStylesAOT(options: NormalizedOptions) { // so instead of transpiling TS(ESM) -> JS(COMMONJS), we transpile JS(ESM + griffel AOT) -> JS(COMMONJS) const overriddenAbsoluteSourceRoot = join(overriddenSourceRoot, esmConfig.outputPath); - return compileSwc(outputConfig, { - ...options, - absoluteSourceRoot: overriddenAbsoluteSourceRoot, - }); + return compileSwc( + outputConfig, + { ...options, absoluteSourceRoot: overriddenAbsoluteSourceRoot }, + cjsRenameTransforms(outputConfig, options), + ); }); return processAsyncQueue(compilationQueue); @@ -241,7 +243,7 @@ export async function compileWithReactCompiler(options: NormalizedOptions) { if (!esmConfig) { logger.warn('es6 module output not specified. Skipping react-compiler...'); const compilationQueue = restOfConfigs.map(outputConfig => { - return compileSwc(outputConfig, options); + return compileSwc(outputConfig, options, cjsRenameTransforms(outputConfig, options)); }); return processAsyncQueue(compilationQueue); } @@ -260,10 +262,11 @@ export async function compileWithReactCompiler(options: NormalizedOptions) { // Transpile from ESM+react-compiler output → CJS (same pattern as Griffel AOT) const overriddenAbsoluteSourceRoot = join(overriddenSourceRoot, esmConfig.outputPath); - return compileSwc(outputConfig, { - ...options, - absoluteSourceRoot: overriddenAbsoluteSourceRoot, - }); + return compileSwc( + outputConfig, + { ...options, absoluteSourceRoot: overriddenAbsoluteSourceRoot }, + cjsRenameTransforms(outputConfig, options), + ); }); return processAsyncQueue(compilationQueue); diff --git a/tools/workspace-plugin/src/executors/build/lib/cjs-extension.spec.ts b/tools/workspace-plugin/src/executors/build/lib/cjs-extension.spec.ts new file mode 100644 index 00000000000000..51ddeae07ed391 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/lib/cjs-extension.spec.ts @@ -0,0 +1,118 @@ +import { mkdtemp, mkdir, writeFile, readFile, rm, access } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { logger } from '@nx/devkit'; + +import { type NormalizedOptions } from './shared'; +import { renameToCjs, cjsRenameTransforms, copyCjsTypes } from './cjs-extension'; + +async function exists(path: string) { + try { + await access(path); + return true; + } catch { + return false; + } +} + +/** + * The `cjs-extension` helpers only act on `"type": "module"` packages. For every other package + * they are a no-op, which keeps them safe to wire into the build executor before any package + * opts into ESM-first packaging. + */ +describe('cjs-extension', () => { + let projectRoot: string; + + beforeEach(async () => { + projectRoot = await mkdtemp(join(tmpdir(), 'cjs-extension-')); + jest.spyOn(logger, 'log').mockImplementation(() => { + return; + }); + }); + + afterEach(async () => { + await rm(projectRoot, { recursive: true, force: true }); + jest.restoreAllMocks(); + }); + + function createOptions(isEsmPackage: boolean): NormalizedOptions { + return { + absoluteProjectRoot: projectRoot, + isEsmPackage, + moduleOutput: [ + { module: 'es6', outputPath: 'lib' }, + { module: 'commonjs', outputPath: 'lib-commonjs' }, + ], + } as unknown as NormalizedOptions; + } + + describe('cjsRenameTransforms', () => { + it('returns [renameToCjs] for the commonjs target of a "type": "module" package', () => { + const options = createOptions(true); + expect(cjsRenameTransforms(options.moduleOutput[1], options)).toEqual([renameToCjs]); + }); + + it('returns undefined for the es6 target, even for a "type": "module" package', () => { + const options = createOptions(true); + expect(cjsRenameTransforms(options.moduleOutput[0], options)).toBeUndefined(); + }); + + it('returns undefined for the commonjs target when the package is not "type": "module"', () => { + const options = createOptions(false); + expect(cjsRenameTransforms(options.moduleOutput[1], options)).toBeUndefined(); + }); + }); + + describe('renameToCjs', () => { + it('renames *.js -> *.cjs and rewrites relative requires + sourceMappingURL comment', async () => { + await mkdir(join(projectRoot, 'lib-commonjs')); + await writeFile( + join(projectRoot, 'lib-commonjs/index.js'), + [`var other = require("./other.js");`, `//# sourceMappingURL=index.js.map`].join('\n'), + ); + + await renameToCjs(join(projectRoot, 'lib-commonjs/index.js')); + + expect(await exists(join(projectRoot, 'lib-commonjs/index.js'))).toBe(false); + const index = await readFile(join(projectRoot, 'lib-commonjs/index.cjs'), 'utf-8'); + expect(index).toContain(`require("./other.cjs")`); + expect(index).toContain(`//# sourceMappingURL=index.cjs.map`); + }); + + it('renames the adjacent *.js.map -> *.cjs.map and patches the "file" field', async () => { + await mkdir(join(projectRoot, 'lib-commonjs')); + await writeFile(join(projectRoot, 'lib-commonjs/index.js.map'), JSON.stringify({ file: 'index.js' })); + + await renameToCjs(join(projectRoot, 'lib-commonjs/index.js.map')); + + expect(await exists(join(projectRoot, 'lib-commonjs/index.js.map'))).toBe(false); + const map = JSON.parse(await readFile(join(projectRoot, 'lib-commonjs/index.cjs.map'), 'utf-8')); + expect(map.file).toBe('index.cjs'); + }); + }); + + describe('copyCjsTypes', () => { + it('is a no-op when the package is not "type": "module"', async () => { + await mkdir(join(projectRoot, 'dist')); + await writeFile(join(projectRoot, 'dist/index.d.ts'), `export declare const a: string;`); + + const result = await copyCjsTypes(createOptions(false)); + + expect(result).toBe(true); + expect(await exists(join(projectRoot, 'dist/index.d.cts'))).toBe(false); + }); + + it('copies rolled *.d.ts -> *.d.cts for "type": "module" packages', async () => { + await mkdir(join(projectRoot, 'dist')); + const dts = `export declare const a: string;`; + await writeFile(join(projectRoot, 'dist/index.d.ts'), dts); + + const result = await copyCjsTypes(createOptions(true)); + + expect(result).toBe(true); + expect(await exists(join(projectRoot, 'dist/index.d.cts'))).toBe(true); + expect(await readFile(join(projectRoot, 'dist/index.d.cts'), 'utf-8')).toBe(dts); + }); + }); +}); diff --git a/tools/workspace-plugin/src/executors/build/lib/cjs-extension.ts b/tools/workspace-plugin/src/executors/build/lib/cjs-extension.ts new file mode 100644 index 00000000000000..0e2438ab8ece16 --- /dev/null +++ b/tools/workspace-plugin/src/executors/build/lib/cjs-extension.ts @@ -0,0 +1,108 @@ +import { readFile, writeFile, rm, readdir, access, copyFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { logger } from '@nx/devkit'; + +import { type NormalizedOptions } from './shared'; +import { type Transform } from './swc'; + +// rewrite only RELATIVE specifiers (./ or ../) ending in .js -> .cjs +const RELATIVE_REQUIRE = /(require\(\s*["'])(\.[^"']+?)\.js(["']\s*\))/g; + +async function exists(path: string) { + try { + await access(path); + return true; + } catch { + return false; + } +} + +async function* walk(dir: string): AsyncGenerator { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) { + yield* walk(p); + } else { + yield p; + } + } +} + +/** + * When a package ships as `"type": "module"`, the CommonJS output (`lib-commonjs`) must use the + * `.cjs` extension - otherwise Node would parse those `.js` files as ESM and fail. + * + * Wired in as an SWC `Transform` (see `cjsRenameTransforms`) so each `commonjs` file is renamed + * right after it's written, instead of a separate postprocessing pass over the whole output dir. + * Renames `*.js` -> `*.cjs` (incl. `*.styles.raw.js`), rewrites relative `require("./x.js")` -> + * `require("./x.cjs")`, and renames the adjacent `*.js.map` -> `*.cjs.map`. + */ +export const renameToCjs: Transform = async filePath => { + if (filePath.endsWith('.js.map')) { + const map = JSON.parse(await readFile(filePath, 'utf-8')); + if (typeof map.file === 'string') { + map.file = map.file.replace(/\.js$/, '.cjs'); + } + await writeFile(filePath.replace(/\.js\.map$/, '.cjs.map'), JSON.stringify(map)); + await rm(filePath); + return; + } + + if (filePath.endsWith('.js')) { + const code = (await readFile(filePath, 'utf-8')).replace(RELATIVE_REQUIRE, '$1$2.cjs$3'); + await writeFile( + filePath.replace(/\.js$/, '.cjs'), + code.replace(/(\/\/#\s*sourceMappingURL=.*?)\.js\.map$/m, '$1.cjs.map'), + ); + await rm(filePath); + } +}; + +/** + * Transforms to pass into `compileSwc` for a given `moduleOutput` entry: `[renameToCjs]` for the + * `commonjs` target of a `"type": "module"` package, `undefined` otherwise (no-op for every other + * package/target, keeping this safe before any package opts into ESM-first packaging). + */ +export function cjsRenameTransforms( + outputConfig: NormalizedOptions['moduleOutput'][number], + options: NormalizedOptions, +): Transform[] | undefined { + return outputConfig.module === 'commonjs' && options.isEsmPackage ? [renameToCjs] : undefined; +} + +/** + * `type: module` packages expose CommonJS types under a `.d.cts` so that `require`-path consumers + * (TypeScript `node16`/`nodenext`) get a CommonJS-flavoured declaration matching the `.cjs` runtime + * file, instead of the ESM-flavoured `.d.ts` (which `@arethetypeswrong/cli` flags as "masquerading"). + * + * Our `dist/*.d.ts` are rolled single-file declarations (api-extractor) using only `export`/`export + * declare` syntax, so a verbatim copy to `.d.cts` is valid in a CommonJS declaration context. + * + * No-op for packages that are not `"type": "module"`. + */ +export async function copyCjsTypes(options: NormalizedOptions): Promise { + if (!options.isEsmPackage) { + return true; + } + + const distDir = join(options.absoluteProjectRoot, 'dist'); + if (!(await exists(distDir))) { + return true; + } + + let copied = 0; + for await (const file of walk(distDir)) { + // copy rolled declarations only (e.g. `index.d.ts`, `unstable.d.ts`), skip `.d.cts`/maps + if (file.endsWith('.d.ts')) { + await copyFile(file, file.replace(/\.d\.ts$/, '.d.cts')); + copied++; + } + } + + if (copied > 0) { + logger.log(`📦 CJS types: ${copied} *.d.ts → *.d.cts in dist`); + } + + return true; +} diff --git a/tools/workspace-plugin/src/executors/build/lib/shared.ts b/tools/workspace-plugin/src/executors/build/lib/shared.ts index 90723f19c49432..aec2185dbae3ca 100644 --- a/tools/workspace-plugin/src/executors/build/lib/shared.ts +++ b/tools/workspace-plugin/src/executors/build/lib/shared.ts @@ -1,4 +1,4 @@ -import { type ExecutorContext, logger } from '@nx/devkit'; +import { type ExecutorContext, logger, readJsonFile } from '@nx/devkit'; import { join } from 'node:path'; import { type BuildExecutorSchema } from '../schema'; @@ -53,6 +53,32 @@ export function normalizeOptions(schema: BuildExecutorSchema, context: ExecutorC const absoluteOutputPathRoot = join(context.root, schema.outputPathRoot); const enableGriffelRawStyles = schema.enableGriffelRawStyles ?? false; const reactCompiler = schema.reactCompiler ?? false; + // TODO: drop the `__esmFirst` half of this once packages flip `"type": "module"` for real (see PR stack #36359-#36362). + // It's a temporary/internal override so the ESM-first postprocessing can be exercised on a package ahead of that migration. + // + // `isEsmPackage` is the single source of truth consumed by every ESM-first postprocessing step: + // `cjsRenameTransforms` (renames `lib-commonjs/*.js` -> `*.cjs` inline during the SWC compile) and + // `copyCjsTypes` (copies rolled `dist/*.d.ts` -> `*.d.cts` after the build) - see `./cjs-extension`. + const pkgJson = readJsonFile<{ type?: string }>(join(absoluteProjectRoot, 'package.json')); + const isEsmPackage = Boolean(schema.__esmFirst) || pkgJson.type === 'module'; + + // The `.cjs` rename only rewrites *relative* `require()` specifiers that already carry an extension + // (e.g. `require("./x.js")` -> `require("./x.cjs")`). Extensionless requires (`require("./x")`) are + // NOT renamed and, worse, won't resolve at runtime once the target file is renamed to `.cjs` - Node's + // implicit extension resolution for `require()` only tries `.js`/`.json`/`.node`, never `.cjs`. + // SWC only emits extensioned relative specifiers when `resolveFully` is on, which we derive from + // `.swcrc`'s `jsc.baseUrl` (see `compileSwc` in `./swc`). So bail loudly instead of shipping a broken build. + if (isEsmPackage) { + const swcConfig = readJsonFile<{ jsc?: { baseUrl?: string } }>(join(absoluteProjectRoot, '.swcrc')); + if (!swcConfig.jsc?.baseUrl) { + throw new Error( + `[build] "${context.projectName}" enables ESM-first postprocessing ("__esmFirst" or "type": "module") ` + + `but its .swcrc is missing "jsc.baseUrl". Without it, SWC won't fully resolve relative import/require ` + + `specifiers, so files renamed to "lib-commonjs/*.cjs" won't be requireable via extensionless specifiers ` + + `at runtime. Add "jsc.baseUrl" to ${join(absoluteProjectRoot, '.swcrc')} before enabling ESM-first output.`, + ); + } + } return { ...defaults, @@ -64,6 +90,7 @@ export function normalizeOptions(schema: BuildExecutorSchema, context: ExecutorC absoluteOutputPathRoot, enableGriffelRawStyles, reactCompiler, + isEsmPackage, workspaceRoot: context.root, }; diff --git a/tools/workspace-plugin/src/executors/build/lib/swc.ts b/tools/workspace-plugin/src/executors/build/lib/swc.ts index 370868830a8b4b..ea4b85a04e51bf 100644 --- a/tools/workspace-plugin/src/executors/build/lib/swc.ts +++ b/tools/workspace-plugin/src/executors/build/lib/swc.ts @@ -75,7 +75,7 @@ export async function compileSwc( } } -type Transform = (filePath: string) => Promise; +export type Transform = (filePath: string) => Promise; async function applyTransforms(filePath: string, transforms?: Array): Promise { if (!transforms || !Array.isArray(transforms) || transforms.length === 0) { return; diff --git a/tools/workspace-plugin/src/executors/build/schema.d.ts b/tools/workspace-plugin/src/executors/build/schema.d.ts index 7bafb80325775c..4058ba93c08da8 100644 --- a/tools/workspace-plugin/src/executors/build/schema.d.ts +++ b/tools/workspace-plugin/src/executors/build/schema.d.ts @@ -44,6 +44,10 @@ export interface BuildExecutorSchema { * When enabled, source is processed with React Compiler as a separate Babel pass before Griffel AOT. */ reactCompiler?: boolean; + /** + * TEMPORARY/INTERNAL: opts this build into ESM-first postprocessing (.cjs extension rewrite + .d.cts types) ahead of the package's own package.json declaring "type": "module". Exists so the infra can be exercised on a package before it actually flips to ESM. Will be removed once the real migration lands and every opted-in package sets "type": "module" itself. + */ + __esmFirst?: boolean; /** * List of static assets. */ diff --git a/tools/workspace-plugin/src/executors/build/schema.json b/tools/workspace-plugin/src/executors/build/schema.json index a416eda7dbd3f4..6c6d34b0899eb5 100644 --- a/tools/workspace-plugin/src/executors/build/schema.json +++ b/tools/workspace-plugin/src/executors/build/schema.json @@ -67,6 +67,11 @@ "description": "Enable babel-plugin-react-compiler processing. When enabled, source is processed with React Compiler as a separate Babel pass before Griffel AOT.", "default": false }, + "__esmFirst": { + "type": "boolean", + "description": "TEMPORARY/INTERNAL: opts this build into ESM-first postprocessing (.cjs extension rewrite + .d.cts types) ahead of the package's own package.json declaring \"type\": \"module\". Exists so the infra can be exercised on a package before it actually flips to ESM. Will be removed once the real migration lands and every opted-in package sets \"type\": \"module\" itself.", + "default": false + }, "assets": { "type": "array", "description": "List of static assets.", diff --git a/tools/workspace-plugin/src/executors/generate-api/lib/utils.ts b/tools/workspace-plugin/src/executors/generate-api/lib/utils.ts index bc169d7fabb7b5..ae526525a0645c 100644 --- a/tools/workspace-plugin/src/executors/generate-api/lib/utils.ts +++ b/tools/workspace-plugin/src/executors/generate-api/lib/utils.ts @@ -8,17 +8,38 @@ import type { PackageJson } from '../../../types'; import type { NormalizedOptions } from '../executor'; import { verboseLog } from './shared'; -function isTypedEntry(exportValue: unknown): exportValue is { types: string } & Record { - return typeof exportValue === 'object' && exportValue !== null && 'types' in exportValue; +/** + * Resolves the declaration (`.d.ts`) types path from an export entry, supporting both the legacy flat + * shape (`{ types }`) and the ESM-first nested shape (`{ import: { types } }`). The `import` condition + * always points at the ESM `.d.ts` rollup, which is what api-extractor consumes. + */ +function getEntryTypes(exportValue: unknown): string | undefined { + if (typeof exportValue !== 'object' || exportValue === null) { + return undefined; + } + const value = exportValue as Record; + if (typeof value.types === 'string') { + return value.types; + } + const importCondition = value.import; + if ( + typeof importCondition === 'object' && + importCondition !== null && + typeof (importCondition as Record).types === 'string' + ) { + return (importCondition as Record).types as string; + } + return undefined; +} + +function isTypedEntry(exportValue: unknown): exportValue is Record { + return getEntryTypes(exportValue) !== undefined; } /** * Checks whether a single export map entry is a wildcard entry with a `types` field. */ -function isWildcardTypedEntry( - exportKey: string, - exportValue: unknown, -): exportValue is { types: string } & Record { +function isWildcardTypedEntry(exportKey: string, exportValue: unknown): exportValue is Record { return exportKey.includes('*') && isTypedEntry(exportValue); } @@ -26,10 +47,7 @@ function isWildcardTypedEntry( * Checks whether a single export map entry is a named (non-wildcard, non-root) entry with a `types` field. * Skips `"."` and `"./package.json"`. */ -function isNamedTypedEntry( - exportKey: string, - exportValue: unknown, -): exportValue is { types: string } & Record { +function isNamedTypedEntry(exportKey: string, exportValue: unknown): exportValue is Record { if (exportKey === '.' || exportKey === './package.json' || exportKey.includes('*')) { return false; } @@ -61,7 +79,7 @@ export function getExportSubpathConfigs(options: NormalizedOptions): IConfigFile for (const [exportKey, exportValue] of Object.entries(exports)) { // Wildcard entries: expand into sub-directories if (isWildcardTypedEntry(exportKey, exportValue)) { - const pathPrefixes = parseWildcardTypesPattern(exportValue.types); + const pathPrefixes = parseWildcardTypesPattern(getEntryTypes(exportValue)!); if (!pathPrefixes) { continue; } @@ -88,7 +106,7 @@ export function getExportSubpathConfigs(options: NormalizedOptions): IConfigFile // Named entries: create config directly from types field if (isNamedTypedEntry(exportKey, exportValue)) { - const parsed = parseNamedTypesPattern(exportValue.types); + const parsed = parseNamedTypesPattern(getEntryTypes(exportValue)!); if (!parsed) { continue; } diff --git a/tools/workspace-plugin/src/types.ts b/tools/workspace-plugin/src/types.ts index 1b0e524daf29fb..3fb7f43d0099c9 100644 --- a/tools/workspace-plugin/src/types.ts +++ b/tools/workspace-plugin/src/types.ts @@ -21,6 +21,7 @@ export interface TsConfig { export interface PackageJson { bin?: string | Record; + type?: 'module' | 'commonjs'; types?: string; typings?: string; private?: boolean; @@ -47,7 +48,17 @@ export interface PackageJson { dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; - exports?: Record>; + exports?: Record< + string, + | string + | Partial<{ + types: string; + style: string; + node: string | { module: string; default: string }; + import: string | { types: string; default: string }; + require: string | { types: string; default: string }; + }> + >; } export interface PackageJsonWithBeachball extends PackageJson { diff --git a/yarn.lock b/yarn.lock index 78e9e50de716e0..f653926088cf89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2572,7 +2572,7 @@ __metadata: resolution: "@fluentui/chart-web-components@workspace:packages/charts/chart-web-components" dependencies: "@fluentui/tokens": "npm:^1.0.0-alpha.23" - "@fluentui/web-components": "npm:^3.0.1" + "@fluentui/web-components": "npm:^3.0.2" "@microsoft/fast-web-utilities": "npm:^6.0.0" "@storybook/html": "npm:9.1.17" "@storybook/html-vite": "npm:9.1.17" @@ -6573,7 +6573,7 @@ __metadata: languageName: unknown linkType: soft -"@fluentui/web-components@npm:*, @fluentui/web-components@npm:^3.0.1, @fluentui/web-components@workspace:packages/web-components": +"@fluentui/web-components@npm:*, @fluentui/web-components@npm:^3.0.2, @fluentui/web-components@workspace:packages/web-components": version: 0.0.0-use.local resolution: "@fluentui/web-components@workspace:packages/web-components" dependencies: