Skip to content

Commit e56135e

Browse files
authored
feat: Adds file size to asset manifest (pixijs#158)
1 parent 938e84f commit e56135e

File tree

6 files changed

+311
-23
lines changed

6 files changed

+311
-23
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on: [ push, pull_request ]
55
jobs:
66
verify:
77
name: CI
8-
runs-on: ubuntu-latest
8+
runs-on: macos-latest
99
strategy:
1010
fail-fast: false
1111
matrix:

packages/assetpack/src/manifest/pixiManifest.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'fs-extra';
2+
import zlib from 'node:zlib';
23
import { BuildReporter, path, stripTags } from '../core/index.js';
34

45
import type { Asset, AssetPipe, PipeSystem, PluginOptions } from '../core/index.js';
@@ -15,11 +16,12 @@ export interface PixiManifest {
1516

1617
export interface PixiManifestEntry {
1718
alias: string | string[];
18-
src: string | string[];
19+
src: (string | { src: string; progressSize?: number })[];
1920
data?: {
2021
// tags: Tags;
2122
[x: string]: any;
2223
};
24+
progressSize?: number;
2325
}
2426

2527
export interface PixiManifestOptions extends PluginOptions {
@@ -39,6 +41,12 @@ export interface PixiManifestOptions extends PluginOptions {
3941
* if true, the metaData will be outputted in the data field of the manifest.
4042
*/
4143
includeMetaData?: boolean;
44+
/**
45+
* if true, the file sizes of each asset will be included in the manifest.
46+
* The sizes are in kilobytes (KB) and represent the gzipped size of each asset.
47+
* @default true
48+
*/
49+
includeFileSizes?: false | 'gzip' | 'raw';
4250
/**
4351
* The name style for asset bundles in the manifest file.
4452
* When set to relative, asset bundles will use their relative paths as names.
@@ -54,7 +62,7 @@ export interface PixiManifestOptions extends PluginOptions {
5462
/** Options to pass to localeCompare. */
5563
collatorOptions?: Intl.CollatorOptions;
5664
}
57-
| ((assetsSrc: string[]) => string[]);
65+
| ((assetsSrc: PixiManifestEntry['src']) => PixiManifestEntry['src']);
5866
/**
5967
* if true, the all tags will be outputted in the data.tags field of the manifest.
6068
* If false, only internal tags will be outputted to the data.tags field. All other tags will be outputted to the data field directly.
@@ -95,6 +103,7 @@ export function pixiManifest(_options: PixiManifestOptions = {}): AssetPipe<Pixi
95103
trimExtensions: false,
96104
includeMetaData: true,
97105
legacyMetaDataOutput: true,
106+
includeFileSizes: false,
98107
nameStyle: 'short',
99108
..._options,
100109
},
@@ -242,25 +251,62 @@ function collectAssets(
242251
}
243252

244253
// Set up sorting options
245-
let sortFn: (assetsSrc: string[]) => string[];
254+
let sortFn: (assetsSrc: PixiManifestEntry['src']) => PixiManifestEntry['src'];
246255

247256
if (typeof options.srcSortOptions === 'function') {
248257
sortFn = options.srcSortOptions;
249258
} else {
250259
const isAscending = options.srcSortOptions?.order === 'ascending';
251260
const collatorOptions = options.srcSortOptions?.collatorOptions;
252261

253-
sortFn = (assetsSrc: string[]) =>
254-
assetsSrc.sort((a, b) =>
255-
(isAscending ? a : b).localeCompare(isAscending ? b : a, undefined, collatorOptions),
256-
);
262+
sortFn = (assetsSrc: PixiManifestEntry['src']) =>
263+
assetsSrc.sort((a, b) => {
264+
const aSrc = typeof a === 'string' ? a : a.src;
265+
const bSrc = typeof b === 'string' ? b : b.src;
266+
267+
return (isAscending ? aSrc : bSrc).localeCompare(
268+
isAscending ? bSrc : aSrc,
269+
undefined,
270+
collatorOptions,
271+
);
272+
});
257273
}
258274

259-
bundleAssets.push({
275+
const bundledAsset: PixiManifestEntry = {
260276
alias: getShortNames(stripTags(path.relative(entryPath, asset.path)), options),
261-
src: sortFn(finalManifestAssets.map((finalAsset) => path.relative(outputPath, finalAsset.path))),
277+
src: sortFn(
278+
finalManifestAssets.map((finalAsset) => {
279+
const src = path.relative(outputPath, finalAsset.path);
280+
let size: number;
281+
282+
if (!options.includeFileSizes) {
283+
return src;
284+
}
285+
286+
try {
287+
if (options.includeFileSizes === 'raw') {
288+
size = fs.statSync(finalAsset.path).size;
289+
} else {
290+
size = zlib.gzipSync(fs.readFileSync(finalAsset.path)).length;
291+
}
292+
size = Number((size / 1024).toFixed(2));
293+
} catch (_e) {
294+
BuildReporter.warn(
295+
`[AssetPack][manifest] Unable to get size for asset '${finalAsset.path}'. Skipping file size entry.`,
296+
);
297+
size = 1;
298+
}
299+
300+
return {
301+
src,
302+
progressSize: size,
303+
};
304+
}),
305+
),
262306
data: options.includeMetaData ? metadata : undefined,
263-
});
307+
};
308+
309+
bundleAssets.push(bundledAsset);
264310
}
265311

266312
asset.children.forEach((child) => {

packages/assetpack/src/spine/spineAtlasManifestMod.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'fs-extra';
2+
import { type PixiManifest } from '..//manifest/pixiManifest.js';
23
import { findAssets, path } from '../core/index.js';
34
import { AtlasView } from './AtlasView.js';
45

@@ -73,11 +74,13 @@ export function spineAtlasManifestMod(_options: SpineManifestOptions = {}): Asse
7374
};
7475
}
7576

76-
function findAndRemoveManifestAsset(manifest: any, assetPath: string) {
77+
function findAndRemoveManifestAsset(manifest: PixiManifest, assetPath: string) {
7778
for (let i = 0; i < manifest.bundles.length; i++) {
7879
const assets = manifest.bundles[i].assets;
7980

80-
const manifestAsset = assets.find((asset: { src: string[] }) => asset.src.includes(assetPath));
81+
const manifestAsset = assets.find((asset) => {
82+
return asset.src.find((src) => (typeof src === 'string' ? src : src.src) === assetPath);
83+
});
8184

8285
if (manifestAsset) {
8386
assets.splice(assets.indexOf(manifestAsset), 1);

packages/assetpack/test/manifest/Manifest.test.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,243 @@ describe('Manifest', () => {
253253
},
254254
],
255255
});
256+
}, 15000);
257+
258+
it('should add filesize information to the manifest', async () => {
259+
const testName = 'manifest-filesize';
260+
const inputDir = getInputDir(pkg, testName);
261+
const outputDir = getOutputDir(pkg, testName);
262+
263+
const useCache = false;
264+
265+
if (!useCache) {
266+
createFolder(pkg, {
267+
name: testName,
268+
files: [],
269+
270+
folders: [
271+
{
272+
name: 'bundle{m}',
273+
files: [
274+
{
275+
name: 'json.json',
276+
content: assetPath('json/json.json'),
277+
},
278+
{
279+
name: 'json.json5',
280+
content: assetPath('json/json.json'),
281+
},
282+
{
283+
name: 'sprite.png',
284+
content: assetPath('image/sp-1.png'),
285+
},
286+
],
287+
folders: [
288+
{
289+
name: 'tps{tps}',
290+
files: genSprites(),
291+
folders: [],
292+
},
293+
],
294+
},
295+
{
296+
name: 'defaultFolder',
297+
files: [
298+
{
299+
name: '1.mp3',
300+
content: assetPath('audio/1.mp3'),
301+
},
302+
{
303+
name: '3.wav',
304+
content: assetPath('audio/3.wav'),
305+
},
306+
],
307+
folders: [],
308+
},
309+
{
310+
name: 'spine',
311+
files: [
312+
{
313+
name: 'dragon{spine}.atlas',
314+
content: assetPath('spine/dragon.atlas'),
315+
},
316+
{
317+
name: 'dragon.json',
318+
content: assetPath('spine/dragon.json'),
319+
},
320+
{
321+
name: 'dragon.png',
322+
content: assetPath('spine/dragon.png'),
323+
},
324+
{
325+
name: 'dragon2.png',
326+
content: assetPath('spine/dragon2.png'),
327+
},
328+
],
329+
folders: [],
330+
},
331+
],
332+
});
333+
}
334+
335+
const assetpack = new AssetPack({
336+
entry: inputDir,
337+
cacheLocation: getCacheDir(pkg, testName),
338+
output: outputDir,
339+
cache: useCache,
340+
pipes: [
341+
audio(),
342+
spineAtlasMipmap(),
343+
texturePacker({
344+
resolutionOptions: {
345+
maximumTextureSize: 512,
346+
},
347+
addFrameNames: true,
348+
}),
349+
mipmap(),
350+
compress({
351+
png: true,
352+
jpg: true,
353+
webp: true,
354+
avif: false,
355+
astc: true,
356+
}),
357+
texturePackerCompress({ astc: true }),
358+
pixiManifest({ includeFileSizes: 'gzip' }),
359+
spineAtlasManifestMod(),
360+
],
361+
});
362+
363+
await assetpack.run();
364+
365+
// load the manifest json
366+
const manifest = sortObjectProperties(await fs.readJSONSync(`${outputDir}/manifest.json`)) as any;
367+
368+
expect(manifest.bundles[1]).toEqual({
369+
name: 'bundle',
370+
assets: [
371+
{
372+
alias: ['bundle/json.json'],
373+
src: [{ src: 'bundle/json.json', progressSize: 0.06 }],
374+
data: {
375+
tags: {
376+
m: true,
377+
},
378+
},
379+
},
380+
{
381+
alias: ['bundle/json.json5'],
382+
src: [{ src: 'bundle/json.json5', progressSize: 0.06 }],
383+
data: {
384+
tags: {
385+
m: true,
386+
},
387+
},
388+
},
389+
{
390+
alias: ['bundle/sprite.png'],
391+
src: [
392+
{ src: 'bundle/sprite@0.5x.webp', progressSize: 3.78 },
393+
{ src: 'bundle/sprite@0.5x.png', progressSize: 5.21 },
394+
{ src: 'bundle/sprite@0.5x.astc.ktx', progressSize: 4.98 },
395+
{ src: 'bundle/sprite.webp', progressSize: 8.2 },
396+
{ src: 'bundle/sprite.png', progressSize: 14.49 },
397+
{ src: 'bundle/sprite.astc.ktx', progressSize: 16.57 },
398+
],
399+
data: {
400+
tags: {
401+
m: true,
402+
},
403+
},
404+
},
405+
{
406+
alias: ['bundle/tps'],
407+
src: [
408+
{ src: 'bundle/tps-0@0.5x.webp.json', progressSize: 0.42 },
409+
{ src: 'bundle/tps-0@0.5x.png.json', progressSize: 0.42 },
410+
{ src: 'bundle/tps-0@0.5x.astc.json', progressSize: 0.43 },
411+
{ src: 'bundle/tps-0.webp.json', progressSize: 0.43 },
412+
{ src: 'bundle/tps-0.png.json', progressSize: 0.43 },
413+
{ src: 'bundle/tps-0.astc.json', progressSize: 0.43 },
414+
],
415+
data: {
416+
tags: {
417+
m: true,
418+
tps: true,
419+
frameNames: [
420+
'sprite9.png',
421+
'sprite8.png',
422+
'sprite7.png',
423+
'sprite6.png',
424+
'sprite5.png',
425+
'sprite4.png',
426+
'sprite3.png',
427+
'sprite2.png',
428+
'sprite1.png',
429+
'sprite0.png',
430+
],
431+
},
432+
frameNames: [
433+
'sprite9.png',
434+
'sprite8.png',
435+
'sprite7.png',
436+
'sprite6.png',
437+
'sprite5.png',
438+
'sprite4.png',
439+
'sprite3.png',
440+
'sprite2.png',
441+
'sprite1.png',
442+
'sprite0.png',
443+
],
444+
},
445+
},
446+
],
447+
});
448+
expect(manifest.bundles[0]).toEqual({
449+
name: 'default',
450+
assets: [
451+
{
452+
alias: ['defaultFolder/1.mp3'],
453+
src: [
454+
{ src: 'defaultFolder/1.ogg', progressSize: 5.87 },
455+
{ src: 'defaultFolder/1.mp3', progressSize: 9.71 },
456+
],
457+
data: {
458+
tags: {},
459+
},
460+
},
461+
{
462+
alias: ['defaultFolder/3.wav'],
463+
src: [
464+
{ src: 'defaultFolder/3.ogg', progressSize: 5.87 },
465+
{ src: 'defaultFolder/3.mp3', progressSize: 10.9 },
466+
],
467+
data: {
468+
tags: {},
469+
},
470+
},
471+
{
472+
alias: ['spine/dragon.json'],
473+
src: [{ src: 'spine/dragon.json', progressSize: 3.31 }],
474+
data: {
475+
tags: {},
476+
},
477+
},
478+
{
479+
alias: ['spine/dragon.atlas'],
480+
src: [
481+
{ src: 'spine/dragon@0.5x.atlas', progressSize: 0.85 },
482+
{ src: 'spine/dragon.atlas', progressSize: 0.87 },
483+
],
484+
data: {
485+
spine: true,
486+
tags: {
487+
spine: true,
488+
},
489+
},
490+
},
491+
],
492+
});
256493
});
257494

258495
it('should copy over files and add them to manifest', async () => {

0 commit comments

Comments
 (0)