Move offline icon storage to app/
Summary: This diff splits Facebook management into a ui-core and electron part: * Cleaned up code and introduces a uniform Icon type to describe a requested icon * Computing icon urls is done in the ui-core * Introduced a RenderHost hook that can transform the request icon into a different url, in this case, a url to load icons from disk in production builds For the browser UI, the urls are currently no rewritten since we have only dev builds (which always used only FB urls already). We could do the same rewrite in the future and download the static assets during build time. But for now this means that in the browser implementation we depend on normal browser caching, with the biggest downside that icons might not appear if the user has no internet connections. With this change we lost our last usage of staticPath computations in ui-core Reviewed By: aigoncharov Differential Revision: D32767426 fbshipit-source-id: d573b6a373e649c7dacd380cf63a50c2dbbd9e70
This commit is contained in:
committed by
Facebook GitHub Bot
parent
86995e0d11
commit
2b2cbb1103
@@ -25,7 +25,8 @@ import {
|
||||
import fs from 'fs';
|
||||
import {setupMenuBar} from './setupMenuBar';
|
||||
import {FlipperServer, FlipperServerConfig} from 'flipper-common';
|
||||
import type {RenderHost} from 'flipper-ui-core';
|
||||
import type {Icon, RenderHost} from 'flipper-ui-core';
|
||||
import {getLocalIconUrl} from '../utils/icons';
|
||||
|
||||
export function initializeElectron(
|
||||
flipperServer: FlipperServer,
|
||||
@@ -186,6 +187,14 @@ export function initializeElectron(
|
||||
path.resolve(flipperServerConfig.paths.staticPath, relativePath)
|
||||
);
|
||||
},
|
||||
getLocalIconUrl(icon: Icon, url: string): string {
|
||||
return getLocalIconUrl(
|
||||
icon,
|
||||
url,
|
||||
flipperServerConfig.paths.appPath,
|
||||
!flipperServerConfig.environmentInfo.isProduction,
|
||||
);
|
||||
},
|
||||
} as RenderHost;
|
||||
|
||||
setupMenuBar();
|
||||
|
||||
65
desktop/app/src/utils/__tests__/icons.node.tsx
Normal file
65
desktop/app/src/utils/__tests__/icons.node.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {buildLocalIconPath, getLocalIconUrl} from '../icons';
|
||||
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
||||
import {getPublicIconUrl} from '../../../../flipper-ui-core/src/utils/icons';
|
||||
import * as path from 'path';
|
||||
import {getRenderHostInstance} from 'flipper-ui-core';
|
||||
import fs from 'fs';
|
||||
|
||||
test('filled icons get correct local path', () => {
|
||||
const iconPath = buildLocalIconPath({
|
||||
name: 'star',
|
||||
variant: 'filled',
|
||||
size: 12,
|
||||
density: 2,
|
||||
});
|
||||
expect(iconPath).toBe(path.join('icons', 'star-filled-12@2x.png'));
|
||||
});
|
||||
|
||||
test('outline icons get correct local path', () => {
|
||||
const iconPath = buildLocalIconPath({
|
||||
name: 'star',
|
||||
variant: 'outline',
|
||||
size: 12,
|
||||
density: 2,
|
||||
});
|
||||
expect(iconPath).toBe(path.join('icons', 'star-outline-12@2x.png'));
|
||||
});
|
||||
|
||||
test('filled icons get correct URL', async () => {
|
||||
const icon = {
|
||||
name: 'star',
|
||||
variant: 'filled',
|
||||
size: 12,
|
||||
density: 2,
|
||||
} as const;
|
||||
const iconUrl = getPublicIconUrl(icon);
|
||||
expect(iconUrl).toBe(
|
||||
'https://facebook.com/assets/?name=star&variant=filled&size=12&set=facebook_icons&density=2x',
|
||||
);
|
||||
const staticPath = getRenderHostInstance().serverConfig.paths.staticPath;
|
||||
const localUrl = getLocalIconUrl(icon, iconUrl, staticPath, false);
|
||||
// since files don't exist at disk in de checkouts
|
||||
expect(localUrl).toBe(iconUrl);
|
||||
|
||||
// ... let's mock a file
|
||||
const iconPath = path.join(staticPath, 'icons', 'star-filled-12@2x.png');
|
||||
try {
|
||||
await fs.promises.writeFile(
|
||||
iconPath,
|
||||
'Generated for unit tests. Please remove',
|
||||
);
|
||||
// should now generate a absolute path
|
||||
expect(getLocalIconUrl(icon, iconUrl, staticPath, false)).toBe(iconPath);
|
||||
} finally {
|
||||
await fs.promises.unlink(iconPath);
|
||||
}
|
||||
});
|
||||
92
desktop/app/src/utils/icons.tsx
Normal file
92
desktop/app/src/utils/icons.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
// We should get rid of sync use entirely but until then the
|
||||
// methods are marked as such.
|
||||
/* eslint-disable node/no-sync */
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type {Icon} from 'flipper-ui-core';
|
||||
|
||||
export type Icons = {
|
||||
[key: string]: Icon['size'][];
|
||||
};
|
||||
|
||||
let _icons: Icons | undefined;
|
||||
|
||||
function getIconsSync(staticPath: string): Icons {
|
||||
return (
|
||||
_icons! ??
|
||||
(_icons = JSON.parse(
|
||||
fs.readFileSync(path.join(staticPath, 'icons.json'), {encoding: 'utf8'}),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
export function buildLocalIconPath(icon: Icon) {
|
||||
return path.join(
|
||||
'icons',
|
||||
`${icon.name}-${icon.variant}-${icon.size}@${icon.density}x.png`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalIconUrl(
|
||||
icon: Icon,
|
||||
url: string,
|
||||
basePath: string,
|
||||
registerIcon: boolean,
|
||||
): string {
|
||||
// resolve icon locally if possible
|
||||
const iconPath = path.join(basePath, buildLocalIconPath(icon));
|
||||
if (fs.existsSync(iconPath)) {
|
||||
return iconPath;
|
||||
}
|
||||
if (registerIcon) {
|
||||
tryRegisterIcon(icon, url, basePath);
|
||||
}
|
||||
|
||||
return url; // fall back to http URL
|
||||
}
|
||||
|
||||
function tryRegisterIcon(icon: Icon, url: string, staticPath: string) {
|
||||
const entryName = icon.name + (icon.variant === 'outline' ? '-outline' : '');
|
||||
const {size} = icon;
|
||||
const icons = getIconsSync(staticPath);
|
||||
if (!icons[entryName]?.includes(size)) {
|
||||
const existing = icons[entryName] || (icons[entryName] = []);
|
||||
if (!existing.includes(size)) {
|
||||
// Check if that icon actually exists!
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
`Trying to use icon '${entryName}' with size ${size} and density ${icon.density}, however the icon doesn't seem to exists at ${url}: ${res.status}`,
|
||||
);
|
||||
}
|
||||
if (!existing.includes(size)) {
|
||||
// the icon exists
|
||||
existing.push(size);
|
||||
existing.sort();
|
||||
fs.writeFileSync(
|
||||
path.join(staticPath, 'icons.json'),
|
||||
JSON.stringify(icons, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
console.warn(
|
||||
`Added uncached icon "${entryName}: [${size}]" to /static/icons.json.`,
|
||||
);
|
||||
} else {
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user