diff --git a/change/@fluentui-react-components-3f1c8a7e-6b02-4d9a-9c41-8e5b0a2d7c14.json b/change/@fluentui-react-components-3f1c8a7e-6b02-4d9a-9c41-8e5b0a2d7c14.json new file mode 100644 index 0000000000000..44c8aad060857 --- /dev/null +++ b/change/@fluentui-react-components-3f1c8a7e-6b02-4d9a-9c41-8e5b0a2d7c14.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: verify bundle isolation of base hooks", + "packageName": "@fluentui/react-components", + "email": "martinhochel@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-headless-components-preview-b41d2e90-7c35-4a18-9f6e-2d0c81a4f7b3.json b/change/@fluentui-react-headless-components-preview-b41d2e90-7c35-4a18-9f6e-2d0c81a4f7b3.json new file mode 100644 index 0000000000000..a5ee5e47aa9b5 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-b41d2e90-7c35-4a18-9f6e-2d0c81a4f7b3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: infer verify-bundle-isolation target from workspace plugin", + "packageName": "@fluentui/react-headless-components-preview", + "email": "martinhochel@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-components/bundle-isolation.config.json b/packages/react-components/react-components/bundle-isolation.config.json new file mode 100644 index 0000000000000..ec85054fa7b42 --- /dev/null +++ b/packages/react-components/react-components/bundle-isolation.config.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../tools/verify-bundle-isolation/schema.json", + "fixturesRoot": "./bundle-size", + "externals": ["react", "react-dom", "react/jsx-runtime", "react/compiler-runtime"], + "forbiddenPackages": ["tabster", "@griffel/*", "@fluentui/react-icons", "@fluentui/react-motion"], + "fixtures": { + "BaseHooks.fixture.js": { + "allowedViolations": ["@fluentui/react-motion", "@griffel/core", "@griffel/react", "tabster"] + } + } +} diff --git a/packages/react-components/react-components/bundle-size/BaseHooks.fixture.js b/packages/react-components/react-components/bundle-size/BaseHooks.fixture.js new file mode 100644 index 0000000000000..07beaa3e59699 --- /dev/null +++ b/packages/react-components/react-components/bundle-size/BaseHooks.fixture.js @@ -0,0 +1,237 @@ +// Named imports only - a namespace import would retain every styled component and defeat the isolation check. +import { + useAccordionBase_unstable, + useAccordionHeaderBase_unstable, + useAccordionPanelBase_unstable, +} from '@fluentui/react-accordion'; +import { + useAvatarBase_unstable, + useAvatarGroupBase_unstable, + useAvatarGroupItemBase_unstable, + useAvatarGroupPopoverBase_unstable, +} from '@fluentui/react-avatar'; +import { + useBadgeBase_unstable, + useCounterBadgeBase_unstable, + usePresenceBadgeBase_unstable, +} from '@fluentui/react-badge'; +import { + useBreadcrumbBase_unstable, + useBreadcrumbButtonBase_unstable, + useBreadcrumbDividerBase_unstable, + useBreadcrumbItemBase_unstable, +} from '@fluentui/react-breadcrumb'; +import { + useButtonBase_unstable, + useCompoundButtonBase_unstable, + useMenuButtonBase_unstable, + useSplitButtonBase_unstable, + useToggleButtonBase_unstable, +} from '@fluentui/react-button'; +import { + useCardBase_unstable, + useCardFooterBase_unstable, + useCardHeaderBase_unstable, + useCardPreviewBase_unstable, +} from '@fluentui/react-card'; +import { useCheckboxBase_unstable } from '@fluentui/react-checkbox'; +import { + useAlphaSliderBase_unstable, + useColorAreaBase_unstable, + useColorPickerBase_unstable, + useColorSliderBase_unstable, +} from '@fluentui/react-color-picker'; +import { useComboboxBase_unstable, useDropdownBase_unstable, useOptionBase_unstable } from '@fluentui/react-combobox'; +import { useDividerBase_unstable } from '@fluentui/react-divider'; +import { useInlineDrawerBase_unstable } from '@fluentui/react-drawer'; +import { useFieldBase_unstable } from '@fluentui/react-field'; +import { useImageBase_unstable } from '@fluentui/react-image'; +import { useInputBase_unstable } from '@fluentui/react-input'; +import { useLabelBase_unstable } from '@fluentui/react-label'; +import { useLinkBase_unstable } from '@fluentui/react-link'; +import { + useMenuBase_unstable, + useMenuItemBase_unstable, + useMenuItemCheckboxBase_unstable, + useMenuItemLinkBase_unstable, + useMenuItemRadioBase_unstable, + useMenuItemSwitchBase_unstable, + useMenuListBase_unstable, + useMenuPopoverBase_unstable, + useMenuTriggerBase_unstable, +} from '@fluentui/react-menu'; +import { useMessageBarBase_unstable } from '@fluentui/react-message-bar'; +import { + useAppItemBase_unstable, + useAppItemStaticBase_unstable, + useNavBase_unstable, + useNavCategoryItemBase_unstable, + useNavItemBase_unstable, + useNavSubItemBase_unstable, + useNavSubItemGroupBase_unstable, +} from '@fluentui/react-nav'; +import { usePersonaBase_unstable } from '@fluentui/react-persona'; +import { useProgressBarBase_unstable } from '@fluentui/react-progress'; +import { useRadioBase_unstable, useRadioGroupBase_unstable } from '@fluentui/react-radio'; +import { + useRatingBase_unstable, + useRatingDisplayBase_unstable, + useRatingItemBase_unstable, +} from '@fluentui/react-rating'; +import { useSearchBoxBase_unstable } from '@fluentui/react-search'; +import { useSelectBase_unstable } from '@fluentui/react-select'; +import { useSkeletonBase_unstable, useSkeletonItemBase_unstable } from '@fluentui/react-skeleton'; +import { useSliderBase_unstable } from '@fluentui/react-slider'; +import { useSpinButtonBase_unstable } from '@fluentui/react-spinbutton'; +import { useSpinnerBase_unstable } from '@fluentui/react-spinner'; +import { + useColorSwatchBase_unstable, + useEmptySwatchBase_unstable, + useImageSwatchBase_unstable, + useSwatchPickerBase_unstable, + useSwatchPickerRowBase_unstable, +} from '@fluentui/react-swatch-picker'; +import { useSwitchBase_unstable } from '@fluentui/react-switch'; +import { useTabBase_unstable, useTabListBase_unstable } from '@fluentui/react-tabs'; +import { + useTagPickerBase_unstable, + useTagPickerButtonBase_unstable, + useTagPickerControlBase_unstable, + useTagPickerInputBase_unstable, +} from '@fluentui/react-tag-picker'; +import { + useInteractionTagBase_unstable, + useInteractionTagPrimaryBase_unstable, + useInteractionTagSecondaryBase_unstable, + useTagBase_unstable, + useTagGroupBase_unstable, +} from '@fluentui/react-tags'; +import { + useTeachingPopoverCarouselBase_unstable, + useTeachingPopoverCarouselFooterButtonBase_unstable, + useTeachingPopoverCarouselNavBase_unstable, + useTeachingPopoverCarouselNavButtonBase_unstable, + useTeachingPopoverFooterBase_unstable, + useTeachingPopoverHeaderBase_unstable, + useTeachingPopoverTitleBase_unstable, +} from '@fluentui/react-teaching-popover'; +import { useTextareaBase_unstable } from '@fluentui/react-textarea'; +import { useToastBase_unstable, useToastBodyBase_unstable, useToastTitleBase_unstable } from '@fluentui/react-toast'; +import { + useToolbarBase_unstable, + useToolbarButtonBase_unstable, + useToolbarDividerBase_unstable, + useToolbarRadioButtonBase_unstable, + useToolbarToggleButtonBase_unstable, +} from '@fluentui/react-toolbar'; +import { useTooltipBase_unstable } from '@fluentui/react-tooltip'; + +console.log( + useAccordionBase_unstable, + useAccordionHeaderBase_unstable, + useAccordionPanelBase_unstable, + useAvatarBase_unstable, + useAvatarGroupBase_unstable, + useAvatarGroupItemBase_unstable, + useAvatarGroupPopoverBase_unstable, + useBadgeBase_unstable, + useCounterBadgeBase_unstable, + usePresenceBadgeBase_unstable, + useBreadcrumbBase_unstable, + useBreadcrumbButtonBase_unstable, + useBreadcrumbDividerBase_unstable, + useBreadcrumbItemBase_unstable, + useButtonBase_unstable, + useCompoundButtonBase_unstable, + useMenuButtonBase_unstable, + useSplitButtonBase_unstable, + useToggleButtonBase_unstable, + useCardBase_unstable, + useCardFooterBase_unstable, + useCardHeaderBase_unstable, + useCardPreviewBase_unstable, + useCheckboxBase_unstable, + useAlphaSliderBase_unstable, + useColorAreaBase_unstable, + useColorPickerBase_unstable, + useColorSliderBase_unstable, + useComboboxBase_unstable, + useDropdownBase_unstable, + useOptionBase_unstable, + useDividerBase_unstable, + useInlineDrawerBase_unstable, + useFieldBase_unstable, + useImageBase_unstable, + useInputBase_unstable, + useLabelBase_unstable, + useLinkBase_unstable, + useMenuBase_unstable, + useMenuItemBase_unstable, + useMenuItemCheckboxBase_unstable, + useMenuItemLinkBase_unstable, + useMenuItemRadioBase_unstable, + useMenuItemSwitchBase_unstable, + useMenuListBase_unstable, + useMenuPopoverBase_unstable, + useMenuTriggerBase_unstable, + useMessageBarBase_unstable, + useAppItemBase_unstable, + useAppItemStaticBase_unstable, + useNavBase_unstable, + useNavCategoryItemBase_unstable, + useNavItemBase_unstable, + useNavSubItemBase_unstable, + useNavSubItemGroupBase_unstable, + usePersonaBase_unstable, + useProgressBarBase_unstable, + useRadioBase_unstable, + useRadioGroupBase_unstable, + useRatingBase_unstable, + useRatingDisplayBase_unstable, + useRatingItemBase_unstable, + useSearchBoxBase_unstable, + useSelectBase_unstable, + useSkeletonBase_unstable, + useSkeletonItemBase_unstable, + useSliderBase_unstable, + useSpinButtonBase_unstable, + useSpinnerBase_unstable, + useColorSwatchBase_unstable, + useEmptySwatchBase_unstable, + useImageSwatchBase_unstable, + useSwatchPickerBase_unstable, + useSwatchPickerRowBase_unstable, + useSwitchBase_unstable, + useTabBase_unstable, + useTabListBase_unstable, + useTagPickerBase_unstable, + useTagPickerButtonBase_unstable, + useTagPickerControlBase_unstable, + useTagPickerInputBase_unstable, + useInteractionTagBase_unstable, + useInteractionTagPrimaryBase_unstable, + useInteractionTagSecondaryBase_unstable, + useTagBase_unstable, + useTagGroupBase_unstable, + useTeachingPopoverCarouselBase_unstable, + useTeachingPopoverCarouselFooterButtonBase_unstable, + useTeachingPopoverCarouselNavBase_unstable, + useTeachingPopoverCarouselNavButtonBase_unstable, + useTeachingPopoverFooterBase_unstable, + useTeachingPopoverHeaderBase_unstable, + useTeachingPopoverTitleBase_unstable, + useTextareaBase_unstable, + useToastBase_unstable, + useToastBodyBase_unstable, + useToastTitleBase_unstable, + useToolbarBase_unstable, + useToolbarButtonBase_unstable, + useToolbarDividerBase_unstable, + useToolbarRadioButtonBase_unstable, + useToolbarToggleButtonBase_unstable, + useTooltipBase_unstable, +); + +export default { + name: 'react-components: all base hooks', +}; diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index ae979fed08df4..f2ebdfa9ae56f 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -607,7 +607,6 @@ }, "devDependencies": { "@fluentui/scripts-cypress": "*", - "@fluentui/verify-bundle-isolation": "*", "@oddbird/popover-polyfill": "^0.6.1" }, "type": "module" diff --git a/packages/react-components/react-headless-components-preview/library/project.json b/packages/react-components/react-headless-components-preview/library/project.json index 7b99dac170d30..ecab81877a1e1 100644 --- a/packages/react-components/react-headless-components-preview/library/project.json +++ b/packages/react-components/react-headless-components-preview/library/project.json @@ -10,20 +10,6 @@ "options": { "exportSubpaths": true } - }, - "verify-bundle-isolation": { - "cache": true, - "dependsOn": ["build", "^build"], - "command": "yarn run -T verify-bundle-isolation", - "options": { - "cwd": "{projectRoot}" - }, - "inputs": ["default", "^default", { "externalDependencies": ["ajv", "webpack"] }], - "outputs": ["{projectRoot}/dist/bundle-isolation"], - "metadata": { - "technologies": ["webpack"], - "description": "Assert entry points do not bundle tabster, Griffel or react-icons" - } } } } diff --git a/tools/verify-bundle-isolation/README.md b/tools/verify-bundle-isolation/README.md index 7dcf1916b5d54..84530c39e8cac 100644 --- a/tools/verify-bundle-isolation/README.md +++ b/tools/verify-bundle-isolation/README.md @@ -25,35 +25,17 @@ no longer uses is not reported. ## Usage -Add the tool as a devDependency of the package to check and give it a target: +Add a `bundle-isolation.config.json` to the package root. That is the whole setup — the workspace plugin infers the +`verify-bundle-isolation` target from the presence of that file, the same way it infers `bundle-size` from a +`bundle-size/` directory. Nothing to add to `project.json`, and the tool does not need to be a devDependency of the +package. -```jsonc -// package.json -{ "devDependencies": { "@fluentui/verify-bundle-isolation": "*" } } ``` - -```jsonc -// project.json -{ - "targets": { - "verify-bundle-isolation": { - "cache": true, - "dependsOn": ["build", "^build"], - "command": "yarn run -T verify-bundle-isolation", - "options": { "cwd": "{projectRoot}" }, - "inputs": ["default", "^default", { "externalDependencies": ["ajv", "webpack"] }], - "outputs": ["{projectRoot}/dist/bundle-isolation"] - } - } -} +yarn nx run :verify-bundle-isolation ``` -The check must run against built output, hence `dependsOn`. It reports an error if bundling resolves to package sources -instead, because the verdict would not reflect what ships. - -`^default` is what makes the cache correct: this task's result depends on every dependency's files, and on the tool -itself, which is a dependency by virtue of the devDependency. Replacing it with a hand-written input list silently -serves stale verdicts after a dependency changes. +The target runs against built output. Bundling that resolves to package sources is reported as an error, because the +verdict would not reflect what ships. The repo pins webpack to a single version through `resolutions` in the root `package.json`. That is deliberate - the verdict is only meaningful if it comes from the same bundler that produces the bundle-size numbers, and webpack 5.109 diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts index 7e9d7a1112c3a..75e8562fa07de 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts @@ -207,6 +207,40 @@ describe(`workspace-plugin`, () => { `); }); + it('should add the verify-bundle-isolation target only when a bundle-isolation.config.json exists', async () => { + await tempFs.createFiles({ + 'with-config/project.json': serializeJson({}), + 'with-config/package.json': serializeJson({}), + 'with-config/bundle-isolation.config.json': serializeJson({}), + 'no-config/project.json': serializeJson({}), + 'no-config/package.json': serializeJson({}), + // a monosize fixtures dir alone must not be mistaken for an isolation opt-in + 'no-config/bundle-size/A.fixture.js': '', + }); + + const withConfig = await createNodesFunction(['with-config/project.json'], options, context); + const noConfig = await createNodesFunction(['no-config/project.json'], options, context); + + expect(getTargetsNames(noConfig, 'no-config')).not.toContain('verify-bundle-isolation'); + expect(getTargets(withConfig, 'with-config')?.['verify-bundle-isolation']).toEqual({ + cache: true, + dependsOn: ['build', '^build'], + command: 'yarn run -T verify-bundle-isolation', + options: { cwd: 'with-config' }, + inputs: [ + 'default', + '^default', + '{workspaceRoot}/tools/verify-bundle-isolation/**', + { externalDependencies: ['ajv', 'webpack'] }, + ], + outputs: ['{projectRoot}/dist/bundle-isolation'], + metadata: { + technologies: ['webpack'], + description: 'Assert entry points do not bundle runtimes the package is meant to stay free of', + }, + }); + }); + describe(`v9 project nodes`, () => { describe(`React Integration Tester config`, () => { it(`should not create atomized targets if project doesnt have required configurations`, async () => { diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.ts index 154f5fe7a0ed3..780ea2be44bef 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.ts @@ -197,6 +197,11 @@ function buildWorkspaceProjectConfiguration( targets['bundle-size'] = bundleSizeTarget; } + const verifyBundleIsolationTarget = buildVerifyBundleIsolationTarget(projectRoot, options, context, config); + if (verifyBundleIsolationTarget) { + targets['verify-bundle-isolation'] = verifyBundleIsolationTarget; + } + // react v9 lib if (config.projectJSON.projectType === 'library' && config.tags.includes('vNext')) { // *-stories projects @@ -566,6 +571,37 @@ function buildBundleSizeTarget( }; } +function buildVerifyBundleIsolationTarget( + projectRoot: string, + options: Required, + context: CreateNodesContextV2, + config: TaskBuilderConfig, +): TargetConfiguration | null { + if (!existsSync(join(projectRoot, 'bundle-isolation.config.json'))) { + return null; + } + + return { + cache: true, + // Must bundle built output - resolving to sources makes the verdict meaningless, and the tool errors on it. + dependsOn: ['build', '^build'], + command: `${config.pmc.exec} verify-bundle-isolation`, + options: { cwd: projectRoot }, + inputs: [ + 'default', + '^default', + // Also what makes `nx affected` select consumers when the checker itself changes. + '{workspaceRoot}/tools/verify-bundle-isolation/**', + { externalDependencies: ['ajv', 'webpack'] }, + ], + outputs: ['{projectRoot}/dist/bundle-isolation'], + metadata: { + technologies: ['webpack'], + description: 'Assert entry points do not bundle runtimes the package is meant to stay free of', + }, + }; +} + function buildE2eTarget( projectRoot: string, options: Required, diff --git a/yarn.lock b/yarn.lock index 29a79676267e6..b5addb6a3c112 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4367,7 +4367,6 @@ __metadata: "@fluentui/react-tooltip": "npm:^9.10.5" "@fluentui/react-utilities": "npm:^9.26.6" "@fluentui/scripts-cypress": "npm:*" - "@fluentui/verify-bundle-isolation": "npm:*" "@oddbird/popover-polyfill": "npm:^0.6.1" "@swc/helpers": "npm:^0.5.1" peerDependencies: