Upgrading react-file-type-icons package for v9#35515
Conversation
…ileextension based filetype icons.
…/bigbadcapers/fluentui into caperez/sep-filetype-icon-update
…ethod used for file extensions
# 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
|
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 |
…mitting with linter fix to ensure the GH Actions pass.
…gbadcapers/fluentui into caperez/filetype-icons-for-v9
…gbadcapers/fluentui into caperez/filetype-icons-for-v9
tudorpopams
left a comment
There was a problem hiding this comment.
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-analyzeris failing — must be green (or confirmed unrelated) before merge.
Warnings (DRY / KISS)
_initializeIconsdpr-variant repetition (DRY).- Reimplemented v8 global icon store with dead
__remapped/__callbacks__fields (KISS/YAGNI). resolveFileTypeIconUrlreverse-engineers the suffix viasplit('_')length 2/3 (KISS, fragile coupling).useFileTypeIcon_unstableresolves icon name/suffix twice (DRY).getFileTypeIconSuffixhand-rolls an SSR window guard instead of reusingcanUseDOM()(KISS/consistency).
Info
- Change-file
type: minorvs 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Dropped:
CALLBACK_STATE_PROP_NAMEconst + the[CALLBACK_STATE_PROP_NAME]: {}writes inmoduleGlobalSettingsandgetGlobalSettings.__remapped: {}initializer ingetIconSettings.- The
__remappedfield on theIconRecordstype.
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('_'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Thanks for the thorough review @tudorpopams — all 7 items addressed in commits
Full CI is green (13/13), including |
Summary
Moves
@fluentui/react-file-type-iconsfrom the v8 layout (packages/react-file-type-icons) to the v9 layout (packages/react-components/react-file-type-icons/library), introduces aFileTypeIconcomponent 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 to9.0.0while keeping its npm name.Notes for v8 consumers upgrading to 9.x
@fluentui/react-file-type-icons, but starts at9.0.0. Apps pinned to^8will not auto-update — opt-in is required.v8release path. The Nx project is taggedvNext, so the package no longer participates in the v8 beachball release; the v8 scope assertion inscripts/beachball/src/config.test.tswas updated accordingly. There is nomaster-based maintenance lane for8.x.getFileTypeIconSuffix. The old implementation evaluatedwin ??= window, which threw under SSR. The new implementation falls back todevicePixelRatio = 1when nowindowis available, so callers running on the server now get the1xsuffix instead of an exception.initializeFileTypeIconsoptions. Theoptions?: Partial<IIconOptions>argument is still accepted and thedisableWarningsflag is honored. Re-registering an already-registered file type icon now logs aconsole.warn(suppressible via{ disableWarnings: true }). Note: unlike the v8@fluentui/style-utilitiesregisterIcons, the v9 implementation overwrites the existing record instead of skipping it. This is intentional and is covered by tests.initializeFileTypeIconswrites toglobalThis.__globalSettings__.iconsusing 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.tsis gone. v9 packages do not callsetVersion(...), so the v8-style duplicate-package warning will not fire if8.xand9.xcopies end up bundled side by side. Avoid installing both simultaneously.Checklist
masterbranchyarn changelocally