Skip to content

Upgrading react-file-type-icons package for v9#35515

Open
bigbadcapers wants to merge 213 commits into
microsoft:masterfrom
bigbadcapers:caperez/filetype-icons-for-v9
Open

Upgrading react-file-type-icons package for v9#35515
bigbadcapers wants to merge 213 commits into
microsoft:masterfrom
bigbadcapers:caperez/filetype-icons-for-v9

Conversation

@bigbadcapers
Copy link
Copy Markdown
Contributor

@bigbadcapers bigbadcapers commented Nov 25, 2025

Summary

Moves @fluentui/react-file-type-icons from the v8 layout (packages/react-file-type-icons) to the v9 layout (packages/react-components/react-file-type-icons/library), introduces a FileTypeIcon component built on the v9 hook/slot/Griffel pattern, and keeps the existing utility surface (initializeFileTypeIcons, getFileTypeIconProps, getFileTypeIconAsHTMLString, getFileTypeIconAsUrl, FileIconType, FileTypeIconMap) intact for current consumers. The package version jumps to 9.0.0 while keeping its npm name.

Notes for v8 consumers upgrading to 9.x

  • Same npm name, major version bump. The package is still published as @fluentui/react-file-type-icons, but starts at 9.0.0. Apps pinned to ^8 will not auto-update — opt-in is required.
  • No more v8 release path. The Nx project is tagged vNext, so the package no longer participates in the v8 beachball release; the v8 scope assertion in scripts/beachball/src/config.test.ts was updated accordingly. There is no master-based maintenance lane for 8.x.
  • SSR robustness in getFileTypeIconSuffix. The old implementation evaluated win ??= window, which threw under SSR. The new implementation falls back to devicePixelRatio = 1 when no window is available, so callers running on the server now get the 1x suffix instead of an exception.
  • initializeFileTypeIcons options. The options?: Partial<IIconOptions> argument is still accepted and the disableWarnings flag is honored. Re-registering an already-registered file type icon now logs a console.warn (suppressible via { disableWarnings: true }). Note: unlike the v8 @fluentui/style-utilities registerIcons, the v9 implementation overwrites the existing record instead of skipping it. This is intentional and is covered by tests.
  • Global icon store shape preserved. The new initializeFileTypeIcons writes to globalThis.__globalSettings__.icons using the same { code, subset: { mergeImageProps: true } } record shape as v8. A v8 host still using @fluentui/react's <Icon iconName="docx16_svg" /> will continue to resolve file type icons correctly regardless of module load order.
  • src/version.ts is gone. v9 packages do not call setVersion(...), so the v8-style duplicate-package warning will not fire if 8.x and 9.x copies end up bundled side by side. Avoid installing both simultaneously.

Checklist

  • Code is up-to-date with the master branch
  • Your changes are covered by tests (if possible)
  • Package based on existing v8 @fluentui/react-file-type-icons with additional error checks, better documentation and testing.
  • You've run yarn change locally

# Conflicts (merged and resolved):
#	packages/react-file-type-icons/src/FileIconType.ts
#	packages/react-file-type-icons/src/FileTypeIconMap.ts
#	packages/react-file-type-icons/src/getFileTypeIconProps.ts
… wrapped in an if statement to filter unwanted properties from the prototype guard-for-in
@bigbadcapers bigbadcapers marked this pull request as ready for review May 15, 2026 22:52
@bigbadcapers bigbadcapers changed the title Creating react-file-type-icons package for v9 Upgrading react-file-type-icons package for v9 May 15, 2026
@bigbadcapers
Copy link
Copy Markdown
Contributor Author

hey @Hotell @JustSlone @tudorpopams please lmk if I should do more rigorous testing on this package move / upgrade to v9 compliant before we can merge. I tested in both v9 storybook and it works as needed, and in the "react-experiments" v8 one to ensure those calling patterns result in identical output to what those apps expect.

But package is now cleanly using v9 semantics, position and documentation, the only thing i have not tested is the full deploy path to npm (will want to make sure we dont break v8 clients), but if there's a way for me to smoke-test that, (or anything else it should undergo) please lmk

Copy link
Copy Markdown
Contributor

@tudorpopams tudorpopams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confidence Score: 60/100

Solid v8→v9 migration with good test/docs coverage, but CI is red and several DRY/KISS smells in the icon-registration and URL-resolution code warrant cleanup before merge. Inline comments below mark each finding.

Summary of findings

Blocker

  • CI react-compiler-analyzer is failing — must be green (or confirmed unrelated) before merge.

Warnings (DRY / KISS)

  • _initializeIcons dpr-variant repetition (DRY).
  • Reimplemented v8 global icon store with dead __remapped / __callbacks__ fields (KISS/YAGNI).
  • resolveFileTypeIconUrl reverse-engineers the suffix via split('_') length 2/3 (KISS, fragile coupling).
  • useFileTypeIcon_unstable resolves icon name/suffix twice (DRY).
  • getFileTypeIconSuffix hand-rolls an SSR window guard instead of reusing canUseDOM() (KISS/consistency).

Info

  • Change-file type: minor vs a comment describing a 9.0.0 major bump — confirm intended.

Category Breakdown

Category Status Notes
Change file PASS 3 change files, well-described
V9 patterns PASS ForwardRefComponent, slots, assertSlots, mergeClasses w/ user className last
Dep layers PASS Only Tier 1/2 deps
SSR safety PASS canUseDOM() + window guards
Testing PASS Tests for utilities + component
API surface PASS etc/*.api.md added; shared-contexts api.md updated
Accessibility PASS aria-label plumbed; alt="" on decorative imgs
Security/Quality WARN DRY/KISS smells noted inline
Docs coverage PASS Stories, README, docsite + examples updated

Recommendation

REQUEST_CHANGES — primarily gated on the red react-compiler-analyzer check. The DRY/KISS items are non-blocking but worth tightening while this is the new long-lived v9 surface. Core architecture, layering, and test/doc coverage are good.


Posted via the /review-pr skill.

};

if (state.root.width === undefined) {
state.root.width = size;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker (CI): react-compiler-analyzer is failing on this PR. The styles hook opts out with 'use no memo', but this hook does not and conditionally mutates state.root.width/state.root.height after slot creation. Please get the analyzer green or confirm the failure is unrelated before merge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at the root: the width/height defaults now flow through getIntrinsicElementProps('img', { ref, width: size, height: size, ...rootProps, src, 'aria-label': ..., 'data-icon-name': ... }), so there's no post-slot mutation. User-supplied width/height in rootProps still win because they spread after the defaults. As a side effect, the analyzer also flagged the 'use no memo' in useFileTypeIconStyles.styles.ts as redundant — auto-removed. react-compiler-analyzer is green in CI.


const options = { extension, type, size, imageFileType };
const iconProps = getFileTypeIconProps(options);
const src = getFileTypeIconAsUrl(options, baseUrl);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY: getFileTypeIconProps(options) (line 25) and getFileTypeIconAsUrl(options, baseUrl) both resolve the icon name + suffix — the latter re-runs getFileTypeIconNameFromExtensionOrType + getFileTypeIconSuffix inside resolveFileTypeIconUrl. That's the same work twice per render. Consider resolving once (e.g. derive iconName/aria-label and src from a single resolveFileTypeIconUrl call).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced both calls with a single resolveFileTypeIconUrl({ extension, type, size, imageFileType }, baseUrl) that returns { src, iconName, ariaLabel, usesPixelRatioDirectory }. The component now resolves icon name + URL + aria-label in one pass. getFileTypeIconProps and getFileTypeIconAsUrl are kept as thin public-API wrappers for legacy callers (no public surface change — etc/react-file-type-icons.api.md is unchanged).

// SVGs scale well, so you can generally use the default image.
// 1.5x is a special case where both SVGs and PNGs need a different image.

fileTypeIcons[type + size + '_1.5x' + PNG_SUFFIX] = createFileTypeIconImage(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY: these six near-identical createFileTypeIconImage(...) blocks (default + _1.5x png/svg + _2x/_3x/_4x png) are easy to get subtly wrong — note 1.5x exists for both png+svg while 2x/3x/4x are png-only, which is hard to verify by eye. Prefer a data-driven loop over a [{ dir, ext }] table so the matrix is declared once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with a declarative table:

const ICON_VARIANTS = [
  { dir: '',      exts: ['png', 'svg'] }, // 1x
  { dir: '_1.5x', exts: ['png', 'svg'] },
  { dir: '_2x',   exts: ['png'] },
  { dir: '_3x',   exts: ['png'] },
  { dir: '_4x',   exts: ['png'] },
] as const;

_initializeIcons now iterates iconTypes × ICON_VARIANTS × exts. The asymmetric matrix is declared once and obvious by inspection. initializeFileTypeIcons.test.tsx (which asserts all the docx16_* / docx16_1.5x_* / docx16_2x_png / etc. keys) still passes unchanged.


type IconRecords = Record<string, unknown> & {
__options: IIconOptions;
__remapped: Record<string, string>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS / YAGNI: __remapped (here) and the __callbacks__ state (CALLBACK_STATE_PROP_NAME, lines 23/27/131) are written but never read anywhere in this package. Only the { code, subset } records under __globalSettings__.icons matter for the documented v8 <Icon> interop. Dropping the unused scaffolding shrinks the surface area of this re-implemented store.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped:

  • CALLBACK_STATE_PROP_NAME const + the [CALLBACK_STATE_PROP_NAME]: {} writes in moduleGlobalSettings and getGlobalSettings.
  • __remapped: {} initializer in getIconSettings.
  • The __remapped field on the IconRecords type.

Kept __options (consumed by disableWarnings) and the { code, subset } per-icon records (the documented v8 <Icon> interop). Tests still pass.

const { extension, size = DEFAULT_ICON_SIZE, type, imageFileType } = options;
const baseIconName = getFileTypeIconNameFromExtensionOrType(extension, type);
const baseSuffix = getFileTypeIconSuffix(size, imageFileType);
const suffixArray = baseSuffix.split('_');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS (fragile coupling): this reverse-engineers the dpr directory by split('_')-ing the output of getFileTypeIconSuffix and branching on length === 2 | 3. URL construction is now implicitly coupled to the exact suffix string format of another function; a format tweak there silently breaks URLs here. Returning the dpr directory directly (instead of re-parsing a string) would be clearer and more robust.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored. New getDevicePixelRatioVariant(size, imageFileType) in getFileTypeIconProps.ts returns { dprDir, ext } directly. Both getFileTypeIconSuffix (which composes the suffix string from those parts) and resolveFileTypeIconUrl (which composes the URL) now share that helper. No string parsing, no implicit coupling — change one and the other follows automatically.

win ??= window;
const devicePixelRatio: number = win.devicePixelRatio;
const resolvedWindow =
win ?? (typeof globalThis !== 'undefined' && 'window' in globalThis ? globalThis.window : undefined);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS / consistency: this hand-rolls window detection (typeof globalThis !== 'undefined' && 'window' in globalThis ? ...). The package already depends on @fluentui/react-utilities and uses canUseDOM() in initializeFileTypeIcons.tsx. Reusing canUseDOM() here would be more readable and consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — getFileTypeIconSuffix / getDevicePixelRatioVariant now use canUseDOM() from @fluentui/react-utilities. Also dropped the unused win?: Window test-seam parameter (no internal callers, never appeared in etc/*.api.md). One small follow-up in 24f94270d3: the lint rule wanted globalThis.window instead of bare window for the post-canUseDOM() access.

@@ -0,0 +1,7 @@
{
"type": "minor",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info: change type is minor, while the comment describes shipping 9.0.0 as a "major bump." The change-files check passes, so beachball accepts it — just confirm this is the intended semver outcome for the relocated package.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried bumping to "type": "major" first, but npx beachball check rejected it: react-file-type-icons/package.json declares disallowedChangeTypes: ["major", "prerelease"] (carried over from the v8 package config), so major is invalid here. Reverted to "type": "minor" in 52f54bf4f5. The published version still goes to 9.0.0 because package.json pins "version": "9.0.0" directly — the change-file type only governs the relative bump beachball would apply, which is moot for the first release at the new major. Happy to drop "major" from disallowedChangeTypes in a follow-up if we want it selectable for future releases.

- Fix react-compiler-analyzer: move width/height defaults into slot.always() call (no post-slot mutation); remove redundant 'use no memo' from styles hook.

- DRY: useFileTypeIcon_unstable now uses single resolveFileTypeIconUrl() returning { src, iconName, ariaLabel }.

- DRY: replace six createFileTypeIconImage blocks in _initializeIcons with a declarative ICON_VARIANTS table.

- KISS/YAGNI: drop unused __remapped, __callbacks__, CALLBACK_STATE_PROP_NAME scaffolding from initializeFileTypeIcons.

- KISS: new shared getDevicePixelRatioVariant() helper drives both getFileTypeIconSuffix and resolveFileTypeIconUrl; no more split('_') reverse-parsing.

- KISS: use canUseDOM() in getFileTypeIconSuffix for SSR guard, consistent with initializeFileTypeIcons.

- Change file: minor -> major to match documented 9.0.0 jump.
@bigbadcapers
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @tudorpopams — all 7 items addressed in commits a1133e1c19, a370fdbca3, 52f54bf4f5, and 24f94270d3. Quick rundown:

  • Blocker (react-compiler-analyzer): fixed root cause — moved width/height defaults into the slot.always(getIntrinsicElementProps(...)) call so the hook no longer mutates state.root.* after slot creation. The analyzer also flagged the 'use no memo' in the styles hook as redundant; auto-removed. Target now green in CI.
  • DRY (component resolver): useFileTypeIcon_unstable now calls a single resolveFileTypeIconUrl() returning { src, iconName, ariaLabel }. Duplicate getFileTypeIconProps + getFileTypeIconAsUrl calls are gone.
  • DRY (_initializeIcons dpr variants): the 6 repeated createFileTypeIconImage blocks are now a single loop over an ICON_VARIANTS = [{ dir, exts }] table. SVG-1x/1.5x vs PNG-full-ladder asymmetry is declared once.
  • KISS / YAGNI (__remapped / __callbacks__): dropped — written but never read. Kept __options + { code, subset } (the documented v8 <Icon> interop).
  • KISS (URL coupling): new shared getDevicePixelRatioVariant(size, imageFileType) → { dprDir, ext } helper drives both getFileTypeIconSuffix and resolveFileTypeIconUrl. No more split('_') round-trip through the suffix string.
  • KISS / consistency (SSR guard): getFileTypeIconSuffix / getDevicePixelRatioVariant use canUseDOM() from @fluentui/react-utilities (with globalThis.window access for the lint rule).
  • Info (change-file semver): tried bumping to major; beachball rejected it because react-file-type-icons/package.json declares disallowedChangeTypes: ["major", "prerelease"] (inherited from the v8 config). Kept minor. Published version still goes to 9.0.0 because package.json pins it directly.

Full CI is green (13/13), including react-compiler-analyzer, e2e, and bundle-size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants