Skip to content
This repository was archived by the owner on May 1, 2020. It is now read-only.

Commit 0dd1b22

Browse files
committed
fix(postprocess): fix and add tests for the logic surrounding purging fonts
1 parent fbad33f commit 0dd1b22

File tree

8 files changed

+228
-106
lines changed

8 files changed

+228
-106
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ionic/app-scripts",
3-
"version": "2.1.4-201709061759",
3+
"version": "2.1.4",
44
"description": "Scripts for Ionic Projects",
55
"homepage": "https://ionicframework.com/",
66
"author": "Ionic Team <[email protected]> (https://ionic.io)",
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { join } from 'path';
2+
3+
import { removeUnusedFonts } from './remove-unused-fonts';
4+
import * as helpers from '../util/helpers';
5+
6+
describe('Remove Fonts', () => {
7+
describe('removeUnusedFonts', () => {
8+
it('should not purge any fonts when target is not cordova', () => {
9+
const fakeFontDirPath = join(process.cwd(), 'www', 'assets', 'fonts');
10+
spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue(fakeFontDirPath);
11+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(getMockFontDirData()));
12+
spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
13+
14+
return removeUnusedFonts({ target: 'notCordova', platform: 'web' }).then(() => {
15+
expect(helpers.getStringPropertyValue).toHaveBeenCalled();
16+
expect(helpers.readDirAsync).toHaveBeenCalledWith(fakeFontDirPath);
17+
expect(helpers.unlinkAsync).not.toHaveBeenCalled();
18+
});
19+
});
20+
21+
it('should purge all non-woffs for ionicons and roboto, and then all of noto-sans for ios', () => {
22+
const fakeFontDirPath = join(process.cwd(), 'www', 'assets', 'fonts');
23+
spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue(fakeFontDirPath);
24+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(getMockFontDirData()));
25+
const unlinkSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
26+
27+
return removeUnusedFonts({ target: 'cordova', platform: 'ios' }).then(() => {
28+
expect(helpers.readDirAsync).toHaveBeenCalledWith(fakeFontDirPath);
29+
expect(unlinkSpy.calls.all()[0].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.eot'));
30+
expect(unlinkSpy.calls.all()[1].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.scss'));
31+
expect(unlinkSpy.calls.all()[2].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.svg'));
32+
expect(unlinkSpy.calls.all()[3].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.ttf'));
33+
expect(unlinkSpy.calls.all()[4].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-bold.ttf'));
34+
expect(unlinkSpy.calls.all()[5].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-bold.woff'));
35+
expect(unlinkSpy.calls.all()[6].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-regular.ttf'));
36+
expect(unlinkSpy.calls.all()[7].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-regular.woff'));
37+
expect(unlinkSpy.calls.all()[8].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans.scss'));
38+
39+
expect(unlinkSpy.calls.all()[9].args[0]).toEqual(join(fakeFontDirPath, 'roboto-bold.ttf'));
40+
expect(unlinkSpy.calls.all()[10].args[0]).toEqual(join(fakeFontDirPath, 'roboto-light.ttf'));
41+
expect(unlinkSpy.calls.all()[11].args[0]).toEqual(join(fakeFontDirPath, 'roboto-medium.ttf'));
42+
expect(unlinkSpy.calls.all()[12].args[0]).toEqual(join(fakeFontDirPath, 'roboto-regular.ttf'));
43+
expect(unlinkSpy.calls.all()[13].args[0]).toEqual(join(fakeFontDirPath, 'roboto.scss'));
44+
});
45+
});
46+
47+
it('should purge all non-woffs for ionicons, all of roboto and noto-sans for android', () => {
48+
const fakeFontDirPath = join(process.cwd(), 'www', 'assets', 'fonts');
49+
spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue(fakeFontDirPath);
50+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(getMockFontDirData()));
51+
const unlinkSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
52+
53+
return removeUnusedFonts({ target: 'cordova', platform: 'android' }).then(() => {
54+
expect(helpers.readDirAsync).toHaveBeenCalledWith(fakeFontDirPath);
55+
expect(unlinkSpy.calls.all()[0].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.eot'));
56+
expect(unlinkSpy.calls.all()[1].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.scss'));
57+
expect(unlinkSpy.calls.all()[2].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.svg'));
58+
expect(unlinkSpy.calls.all()[3].args[0]).toEqual(join(fakeFontDirPath, 'ionicons.ttf'));
59+
expect(unlinkSpy.calls.all()[4].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-bold.ttf'));
60+
expect(unlinkSpy.calls.all()[5].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-bold.woff'));
61+
expect(unlinkSpy.calls.all()[6].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-regular.ttf'));
62+
expect(unlinkSpy.calls.all()[7].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans-regular.woff'));
63+
expect(unlinkSpy.calls.all()[8].args[0]).toEqual(join(fakeFontDirPath, 'noto-sans.scss'));
64+
65+
expect(unlinkSpy.calls.all()[9].args[0]).toEqual(join(fakeFontDirPath, 'roboto-bold.ttf'));
66+
expect(unlinkSpy.calls.all()[10].args[0]).toEqual(join(fakeFontDirPath, 'roboto-bold.woff'));
67+
expect(unlinkSpy.calls.all()[11].args[0]).toEqual(join(fakeFontDirPath, 'roboto-bold.woff2'));
68+
expect(unlinkSpy.calls.all()[12].args[0]).toEqual(join(fakeFontDirPath, 'roboto-light.ttf'));
69+
expect(unlinkSpy.calls.all()[13].args[0]).toEqual(join(fakeFontDirPath, 'roboto-light.woff'));
70+
expect(unlinkSpy.calls.all()[14].args[0]).toEqual(join(fakeFontDirPath, 'roboto-light.woff2'));
71+
expect(unlinkSpy.calls.all()[15].args[0]).toEqual(join(fakeFontDirPath, 'roboto-medium.ttf'));
72+
expect(unlinkSpy.calls.all()[16].args[0]).toEqual(join(fakeFontDirPath, 'roboto-medium.woff'));
73+
expect(unlinkSpy.calls.all()[17].args[0]).toEqual(join(fakeFontDirPath, 'roboto-medium.woff2'));
74+
expect(unlinkSpy.calls.all()[18].args[0]).toEqual(join(fakeFontDirPath, 'roboto-regular.ttf'));
75+
expect(unlinkSpy.calls.all()[19].args[0]).toEqual(join(fakeFontDirPath, 'roboto-regular.woff'));
76+
expect(unlinkSpy.calls.all()[20].args[0]).toEqual(join(fakeFontDirPath, 'roboto-regular.woff2'));
77+
expect(unlinkSpy.calls.all()[21].args[0]).toEqual(join(fakeFontDirPath, 'roboto.scss'));
78+
79+
});
80+
});
81+
82+
it('should purge all non-woffs for ionicons, all of roboto and noto-sans for windows', () => {
83+
const fakeFontDirPath = join(process.cwd(), 'www', 'assets', 'fonts');
84+
spyOn(helpers, helpers.getStringPropertyValue.name).and.returnValue(fakeFontDirPath);
85+
spyOn(helpers, helpers.readDirAsync.name).and.returnValue(Promise.resolve(getMockFontDirData()));
86+
const unlinkSpy = spyOn(helpers, helpers.unlinkAsync.name).and.returnValue(Promise.resolve());
87+
88+
return removeUnusedFonts({ target: 'cordova', platform: 'windows' }).then(() => {
89+
expect(helpers.readDirAsync).toHaveBeenCalledWith(fakeFontDirPath);
90+
expect(helpers.unlinkAsync).not.toHaveBeenCalled();
91+
});
92+
});
93+
});
94+
});
95+
96+
function getMockFontDirData() {
97+
return [
98+
'ionicons.eot',
99+
'ionicons.scss',
100+
'ionicons.svg',
101+
'ionicons.ttf',
102+
'ionicons.woff',
103+
'ionicons.woff2',
104+
'noto-sans-bold.ttf',
105+
'noto-sans-bold.woff',
106+
'noto-sans-regular.ttf',
107+
'noto-sans-regular.woff',
108+
'noto-sans.scss',
109+
'roboto-bold.ttf',
110+
'roboto-bold.woff',
111+
'roboto-bold.woff2',
112+
'roboto-light.ttf',
113+
'roboto-light.woff',
114+
'roboto-light.woff2',
115+
'roboto-medium.ttf',
116+
'roboto-medium.woff',
117+
'roboto-medium.woff2',
118+
'roboto-regular.ttf',
119+
'roboto-regular.woff',
120+
'roboto-regular.woff2',
121+
'roboto.scss',
122+
'my-custom-font.eot',
123+
'my-custom-font.scss',
124+
'my-custom-font.svg',
125+
'my-custom-font.ttf',
126+
'my-custom-font.woff',
127+
'my-custom-font.woff2'
128+
];
129+
}
Lines changed: 59 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,68 @@
1-
import { BuildContext } from '../util/interfaces';
2-
import { join } from 'path';
3-
import { Logger } from '../logger/logger';
4-
import { unlinkAsync } from '../util/helpers';
5-
import * as glob from 'glob';
6-
7-
8-
export function removeUnusedFonts(context: BuildContext) {
9-
// For webapps, we pretty much need all fonts to be available because
10-
// the web server deployment never knows which browser/platform is
11-
// opening the app. Additionally, webapps will request fonts on-demand,
12-
// so having them all sit in the www/assets/fonts directory doesn’t
13-
// hurt anything if it’s never being requested.
14-
15-
// However, with Cordova, the entire directory gets bundled and
16-
// shipped in the ipa/apk, but we also know exactly which platform
17-
// is opening the webapp. For this reason we can safely delete font
18-
// files we know would never be opened by the platform. So app-scripts
19-
// will continue to copy all font files over, but the cordova build
20-
// process would delete those we know are useless and just taking up
21-
// space. End goal is that the Cordova ipa/apk filesize is smaller.
1+
import { extname, join } from 'path';
222

23-
// Font Format Support:
24-
// ttf: http://caniuse.com/#feat=ttf
25-
// woff: http://caniuse.com/#feat=woff
26-
// woff2: http://caniuse.com/#feat=woff2
27-
28-
if (context.target === 'cordova') {
29-
const fontsRemoved: string[] = [];
30-
// all cordova builds should remove .eot, .svg, .ttf, and .scss files
31-
fontsRemoved.push('*.eot');
32-
fontsRemoved.push('*.ttf');
33-
fontsRemoved.push('*.svg');
34-
fontsRemoved.push('*.scss');
3+
import { Logger } from '../logger/logger';
4+
import * as Constants from '../util/constants';
5+
import { getStringPropertyValue, readDirAsync, unlinkAsync } from '../util/helpers';
6+
import { BuildContext } from '../util/interfaces';
357

36-
// all cordova builds should remove Noto-Sans
37-
// Only windows would use Noto-Sans, and it already comes with
38-
// a system font so it wouldn't need our own copy.
39-
fontsRemoved.push('noto-sans*');
408

41-
if (context.platform === 'android') {
42-
// Remove all Roboto fonts. Android already comes with Roboto system
43-
// fonts so shipping our own is unnecessary. Including roboto fonts
44-
// is only useful for PWAs and during development.
45-
fontsRemoved.push('roboto*');
9+
// For webapps, we pretty much need all fonts to be available because
10+
// the web server deployment never knows which browser/platform is
11+
// opening the app. Additionally, webapps will request fonts on-demand,
12+
// so having them all sit in the www/assets/fonts directory doesn’t
13+
// hurt anything if it’s never being requested.
14+
15+
// However, with Cordova, the entire directory gets bundled and
16+
// shipped in the ipa/apk, but we also know exactly which platform
17+
// is opening the webapp. For this reason we can safely delete font
18+
// files we know would never be opened by the platform. So app-scripts
19+
// will continue to copy all font files over, but the cordova build
20+
// process would delete those we know are useless and just taking up
21+
// space. End goal is that the Cordova ipa/apk filesize is smaller.
22+
23+
// Font Format Support:
24+
// ttf: http://caniuse.com/#feat=ttf
25+
// woff: http://caniuse.com/#feat=woff
26+
// woff2: http://caniuse.com/#feat=woff2
27+
export function removeUnusedFonts(context: BuildContext): Promise<any> {
28+
const fontDir = getStringPropertyValue(Constants.ENV_VAR_FONTS_DIR);
29+
return readDirAsync(fontDir).then((fileNames: string[]) => {
30+
fileNames = fileNames.sort();
31+
const toPurge = getFontFileNamesToPurge(context.target, context.platform, fileNames);
32+
const fullPaths = toPurge.map(fileName => join(fontDir, fileName));
33+
const promises = fullPaths.map(fullPath => unlinkAsync(fullPath));
34+
return Promise.all(promises);
35+
});
36+
}
4637

47-
} else if (context.platform === 'ios') {
48-
// Keep Roboto for now. Apps built for iOS may still use Material Design,
49-
// so in that case Roboto should be available. Later we can improve the
50-
// CLI to be smarter and read the user’s ionic config. Also, the roboto
51-
// fonts themselves are pretty small.
38+
export function getFontFileNamesToPurge(target: string, platform: string, fileNames: string[]): string[] {
39+
if (target !== Constants.CORDOVA) {
40+
return [];
41+
}
42+
const filesToDelete = new Set<string>();
43+
for (const fileName of fileNames) {
44+
if (platform === 'android') {
45+
// remove noto-sans, roboto, and non-woff ionicons
46+
if (fileName.startsWith('noto-sans') || fileName.startsWith('roboto') || (isIonicons(fileName) && !isWoof(fileName))) {
47+
filesToDelete.add(fileName);
48+
}
49+
} else if (platform === 'ios') {
50+
// remove noto-sans, non-woff ionicons
51+
if (fileName.startsWith('noto-sans') || (fileName.startsWith('roboto') && !isWoof(fileName)) || (isIonicons(fileName) && !isWoof(fileName))) {
52+
filesToDelete.add(fileName);
53+
}
5254
}
55+
// for now don't bother deleting anything for windows, need to get some info first
5356

54-
let filesToDelete: string[] = [];
55-
56-
let promises = fontsRemoved.map(pattern => {
57-
return new Promise(resolve => {
58-
let searchPattern = join(context.wwwDir, 'assets', 'fonts', pattern);
59-
60-
glob(searchPattern, (err, files) => {
61-
if (err) {
62-
Logger.error(`removeUnusedFonts: ${err}`);
63-
64-
} else {
65-
files.forEach(f => {
66-
if (filesToDelete.indexOf(f) === -1) {
67-
filesToDelete.push(f);
68-
}
69-
});
70-
}
71-
72-
resolve();
73-
});
74-
75-
});
76-
});
77-
78-
return Promise.all(promises).then(() => {
79-
return unlinkAsync(filesToDelete).then(() => {
80-
if (filesToDelete.length) {
81-
Logger.info(`removed unused font files`);
82-
return true;
83-
}
84-
return false;
85-
});
86-
});
8757
}
58+
return Array.from(filesToDelete);
59+
}
60+
61+
function isIonicons(fileName: string) {
62+
return fileName.startsWith('ionicons');
63+
}
8864

89-
// nothing to do here, carry on
90-
return Promise.resolve();
65+
// woof woof
66+
function isWoof(fileName: string) {
67+
return extname(fileName) === '.woff' || extname(fileName) === '.woff2';
9168
}

src/postprocess.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,32 @@ export function postprocess(context: BuildContext) {
2222

2323

2424
function postprocessWorker(context: BuildContext) {
25-
return Promise.all([
26-
purgeSourceMapsIfNeeded(context),
27-
removeUnusedFonts(context),
28-
updateIndexHtml(context),
29-
writeFilesToDisk(context)
30-
]);
31-
}
25+
const promises: Promise<any>[] = [];
26+
promises.push(purgeSourceMapsIfNeeded(context));
27+
promises.push(updateIndexHtml(context));
3228

33-
export function writeFilesToDisk(context: BuildContext) {
3429
if (getBooleanPropertyValue(Constants.ENV_AOT_WRITE_TO_DISK)) {
35-
emptyDirSync(context.tmpDir);
36-
const files = context.fileCache.getAll();
37-
files.forEach(file => {
38-
const dirName = dirname(file.path);
39-
const relativePath = relative(process.cwd(), dirName);
40-
const tmpPath = join(context.tmpDir, relativePath);
41-
const fileName = basename(file.path);
42-
const fileToWrite = join(tmpPath, fileName);
43-
mkdirpSync(tmpPath);
44-
writeFileSync(fileToWrite, file.content);
45-
});
30+
promises.push(writeFilesToDisk(context));
31+
}
32+
33+
if (context.optimizeJs && getBooleanPropertyValue(Constants.ENV_PURGE_UNUSED_FONTS)) {
34+
promises.push(removeUnusedFonts(context));
4635
}
36+
37+
return Promise.all(promises);
38+
}
39+
40+
export function writeFilesToDisk(context: BuildContext) {
41+
emptyDirSync(context.tmpDir);
42+
const files = context.fileCache.getAll();
43+
files.forEach(file => {
44+
const dirName = dirname(file.path);
45+
const relativePath = relative(process.cwd(), dirName);
46+
const tmpPath = join(context.tmpDir, relativePath);
47+
const fileName = basename(file.path);
48+
const fileToWrite = join(tmpPath, fileName);
49+
mkdirpSync(tmpPath);
50+
writeFileSync(fileToWrite, file.content);
51+
});
4752
return Promise.resolve();
4853
}

src/util/config.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe('config', () => {
7474
expect(context.wwwDir).toEqual(join(process.cwd(), Constants.WWW_DIR));
7575
expect(context.wwwIndex).toEqual('index.html');
7676
expect(context.buildDir).toEqual(join(process.cwd(), Constants.WWW_DIR, Constants.BUILD_DIR));
77+
expect(fakeConfig[Constants.ENV_VAR_FONTS_DIR]).toEqual(join(context.wwwDir, 'assets', 'fonts'));
7778
expect(context.pagesDir).toEqual(join(context.srcDir, 'pages'));
7879
expect(context.componentsDir).toEqual(join(context.srcDir, 'components'));
7980
expect(context.directivesDir).toEqual(join(context.srcDir, 'directives'));

src/util/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export function generateContext(context?: BuildContext): BuildContext {
9090
setProcessEnvVar(Constants.ENV_VAR_BUILD_DIR, context.buildDir);
9191
Logger.debug(`buildDir set to ${context.buildDir}`);
9292

93+
const fontsDir = resolve(getConfigValue(context, '--fontsDir', null, Constants.ENV_VAR_FONTS_DIR, Constants.ENV_VAR_FONTS_DIR.toLowerCase(), join(context.wwwDir, 'assets', 'fonts')));
94+
setProcessEnvVar(Constants.ENV_VAR_FONTS_DIR, fontsDir);
95+
Logger.debug(`fontsDir set to ${fontsDir}`);
96+
9397
context.sourcemapDir = resolve(context.sourcemapDir || getConfigValue(context, '--sourcemapDir', null, Constants.ENV_VAR_SOURCEMAP_DIR, Constants.ENV_VAR_SOURCEMAP_DIR.toLowerCase(), Constants.SOURCEMAP_DIR));
9498
setProcessEnvVar(Constants.ENV_VAR_SOURCEMAP_DIR, context.sourcemapDir);
9599
Logger.debug(`sourcemapDir set to ${context.sourcemapDir}`);
@@ -276,6 +280,10 @@ export function generateContext(context?: BuildContext): BuildContext {
276280
setProcessEnvVar(Constants.ENV_POLYFILL_FILE_NAME, polyfillName);
277281
Logger.debug(`polyfillName set to ${polyfillName}`);
278282

283+
const purgeUnusedFonts = getConfigValue(context, '--purgeUnusedFonts', null, Constants.ENV_PURGE_UNUSED_FONTS, Constants.ENV_PURGE_UNUSED_FONTS.toLowerCase(), 'true');
284+
setProcessEnvVar(Constants.ENV_PURGE_UNUSED_FONTS, purgeUnusedFonts);
285+
Logger.debug(`purgeUnusedFonts set to ${purgeUnusedFonts}`);
286+
279287
/* Provider Path Stuff */
280288
setProcessEnvVar(Constants.ENV_ACTION_SHEET_CONTROLLER_CLASSNAME, 'ActionSheetController');
281289
setProcessEnvVar(Constants.ENV_ACTION_SHEET_CONTROLLER_PATH, join(context.ionicAngularDir, 'components', 'action-sheet', 'action-sheet-controller.js'));

src/util/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const PROVIDER = 'provider';
2222
export const TABS = 'tabs';
2323
export const AT_ANGULAR = '@angular';
2424
export const RXJS = 'rxjs';
25+
export const CORDOVA = 'cordova';
2526

2627
export const ENV_VAR_PROD = 'prod';
2728
export const ENV_VAR_DEV = 'dev';
@@ -36,6 +37,7 @@ export const ENV_VAR_PIPES_DIR = 'IONIC_PIPES_DIR';
3637
export const ENV_VAR_PROVIDERS_DIR = 'IONIC_PROVIDERS_DIR';
3738
export const ENV_VAR_TMP_DIR = 'IONIC_TMP_DIR';
3839
export const ENV_VAR_WWW_DIR = 'IONIC_WWW_DIR';
40+
export const ENV_VAR_FONTS_DIR = 'IONIC_FONTS_DIR';
3941
export const ENV_VAR_SOURCEMAP_DIR = 'IONIC_SOURCEMAP_DIR';
4042
export const ENV_VAR_HTML_TO_SERVE = 'IONIC_HTML_TO_SERVE';
4143
export const ENV_VAR_BUILD_DIR = 'IONIC_BUILD_DIR';
@@ -83,7 +85,7 @@ export const ENV_NG_MODULE_FILE_NAME_SUFFIX = 'IONIC_NG_MODULE_FILENAME_SUFFIX';
8385
export const ENV_POLYFILL_FILE_NAME = 'IONIC_POLYFILL_FILE_NAME';
8486
export const ENV_PRINT_WEBPACK_DEPENDENCY_TREE = 'IONIC_PRINT_WEBPACK_DEPENDENCY_TREE';
8587
export const ENV_PARSE_DEEPLINKS = 'IONIC_PARSE_DEEPLINKS';
86-
88+
export const ENV_PURGE_UNUSED_FONTS = 'IONIC_PURGE_UNUSED_FONTS';
8789

8890

8991
/* Providers */

0 commit comments

Comments
 (0)