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:
Michel Weststrate
2021-12-08 04:25:28 -08:00
committed by Facebook GitHub Bot
parent 86995e0d11
commit 2b2cbb1103
13 changed files with 285 additions and 289 deletions

View File

@@ -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();

View 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);
}
});

View 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));
}
}
}