diff --git a/package-lock.json b/package-lock.json index 2cad1102..3f11da58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32140,7 +32140,7 @@ }, "packages/assetpack": { "name": "@assetpack/core", - "version": "1.1.1", + "version": "1.2.1", "license": "MIT", "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", @@ -32163,6 +32163,7 @@ "fs-extra": "^11.2.0", "glob": "^10.4.1", "gpu-tex-enc": "^1.2.5", + "json5": "^2.2.3", "maxrects-packer": "^2.7.3", "merge": "^2.1.1", "minimatch": "9.0.4", diff --git a/packages/assetpack/package.json b/packages/assetpack/package.json index d817a8c3..0e2d8486 100644 --- a/packages/assetpack/package.json +++ b/packages/assetpack/package.json @@ -1,15 +1,15 @@ { "name": "@assetpack/core", - "version": "1.1.1", - "license": "MIT", - "type": "module", + "version": "1.2.1", + "keywords": [], "homepage": "https://pixijs.io/assetpack/", "bugs": "https://github.com/pixijs/assetpack/issues", "repository": { "type": "git", "url": "https://github.com/pixijs/assetpack.git" }, - "keywords": [], + "license": "MIT", + "type": "module", "exports": { ".": "./dist/core/index.js", "./cache-buster": "./dist/cache-buster/index.js", @@ -35,8 +35,8 @@ ], "scripts": { "build": "tsc", - "release": "xs bump,git-push", "publish-ci": "xs publish", + "release": "xs bump,git-push", "watch": "tsc -w" }, "husky": { @@ -70,6 +70,7 @@ "fs-extra": "^11.2.0", "glob": "^10.4.1", "gpu-tex-enc": "^1.2.5", + "json5": "^2.2.3", "maxrects-packer": "^2.7.3", "merge": "^2.1.1", "minimatch": "9.0.4", diff --git a/packages/assetpack/src/core/AssetCache.ts b/packages/assetpack/src/core/AssetCache.ts index 1afc27ef..f1a4ffe6 100644 --- a/packages/assetpack/src/core/AssetCache.ts +++ b/packages/assetpack/src/core/AssetCache.ts @@ -51,18 +51,7 @@ export class AssetCache private _serializeAsset(asset: Asset, schema: AssetCacheData['assets'], saveHash = false) { - const serializeAsset: CachedAsset = { - isFolder: asset.isFolder, - parent: asset.parent?.path, - transformParent: asset.transformParent?.path, - metaData: asset.metaData, - transformData: asset.transformData - }; - - if (!asset.isFolder && saveHash) - { - serializeAsset.hash = asset.hash; - } + const serializeAsset: CachedAsset = this.toCacheData(asset, saveHash); schema[asset.path] = serializeAsset; @@ -77,6 +66,25 @@ export class AssetCache this._serializeAsset(child, schema); }); } + + private toCacheData(asset: Asset, saveHash: boolean): CachedAsset + { + const data: CachedAsset = { + isFolder: asset.isFolder, + parent: asset.parent?.path, + transformParent: asset.transformParent?.path, + metaData: { ...asset.metaData }, + inheritedMetaData: { ...asset.inheritedMetaData }, + transformData: { ...asset.transformData } + }; + + if (!asset.isFolder && saveHash) + { + data.hash = asset.hash; + } + + return data; + } } export interface CachedAsset @@ -85,6 +93,7 @@ export interface CachedAsset hash?: string; parent: string | undefined; metaData: Record; + inheritedMetaData: Record; transformData: Record; transformParent: string | undefined; } diff --git a/packages/assetpack/src/core/AssetPack.ts b/packages/assetpack/src/core/AssetPack.ts index 29ecca79..0d0f13a7 100644 --- a/packages/assetpack/src/core/AssetPack.ts +++ b/packages/assetpack/src/core/AssetPack.ts @@ -129,7 +129,7 @@ export class AssetPack if (cache) { // write back to the cache... - await (assetCache as AssetCache).write(root); + (assetCache as AssetCache).write(root); // release the buffers from the cache root.releaseChildrenBuffers(); diff --git a/packages/assetpack/src/core/AssetWatcher.ts b/packages/assetpack/src/core/AssetWatcher.ts index 3bf8f7e5..b790d080 100644 --- a/packages/assetpack/src/core/AssetWatcher.ts +++ b/packages/assetpack/src/core/AssetWatcher.ts @@ -18,7 +18,7 @@ export interface AssetWatcherOptions assetSettingsData?: AssetSettings[]; ignore?: string | string[]; onUpdate: (root: Asset) => Promise; - onComplete: (root: Asset) => Promise; + onComplete: (root: Asset) => void; } interface ChangeData diff --git a/packages/assetpack/src/core/utils/syncAssetsWithCache.ts b/packages/assetpack/src/core/utils/syncAssetsWithCache.ts index eadaaeac..d3a58492 100644 --- a/packages/assetpack/src/core/utils/syncAssetsWithCache.ts +++ b/packages/assetpack/src/core/utils/syncAssetsWithCache.ts @@ -26,6 +26,8 @@ function syncAssetsFromCache(assetHash: Record, cachedData: Recor }); assetToDelete.metaData = cachedAsset.metaData; + assetToDelete.transformData = cachedAsset.transformData; + assetToDelete.inheritedMetaData = cachedAsset.inheritedMetaData; assetToDelete.state = 'deleted'; @@ -87,22 +89,24 @@ function syncTransformedAssetsFromCache(assetHash: Record, cached for (const i in cachedData) { const cachedAssetData = cachedData[i]; + let asset = assetHash[i]; - if (cachedAssetData.transformParent) + if (!asset) { - const transformedAsset = new Asset({ + asset = new Asset({ path: i, isFolder: cachedAssetData.isFolder }); - transformedAsset.metaData = cachedAssetData.metaData; - transformedAsset.transformData = cachedAssetData.transformData; + transformedAssets[i] = asset; + assetHash[i] = asset; - transformedAssets[i] = transformedAsset; - assetHash[i] = transformedAsset; - - transformedAsset.transformParent = assetHash[cachedAssetData.transformParent]; + asset.transformParent = assetHash[cachedAssetData.transformParent!]; } + + asset.inheritedMetaData = cachedAssetData.inheritedMetaData; + asset.transformData = cachedAssetData.transformData; + asset.metaData = cachedAssetData.metaData; } for (const i in transformedAssets) diff --git a/packages/assetpack/src/json/index.ts b/packages/assetpack/src/json/index.ts index 7835f273..cd43a863 100644 --- a/packages/assetpack/src/json/index.ts +++ b/packages/assetpack/src/json/index.ts @@ -1,3 +1,4 @@ +import json5 from 'json5'; import { checkExt, createNewAssetAt, Logger } from '../core/index.js'; import type { Asset, AssetPipe } from '../core/index.js'; @@ -13,14 +14,18 @@ export function json(): AssetPipe }, test(asset: Asset) { - return !asset.metaData[this.tags!.nc] && checkExt(asset.path, '.json'); + return !asset.metaData[this.tags!.nc] && checkExt(asset.path, '.json', '.json5'); }, async transform(asset: Asset) { try { - const json = JSON.parse(asset.buffer.toString()); - const compressedJsonAsset = createNewAssetAt(asset, asset.filename); + const json = json5.parse(asset.buffer.toString()); + + // replace the json5 with json + const filename = asset.filename.replace('.json5', '.json'); + + const compressedJsonAsset = createNewAssetAt(asset, filename); compressedJsonAsset.buffer = Buffer.from(JSON.stringify(json)); diff --git a/packages/assetpack/src/manifest/pixiManifest.ts b/packages/assetpack/src/manifest/pixiManifest.ts index d439f517..2013a1b5 100644 --- a/packages/assetpack/src/manifest/pixiManifest.ts +++ b/packages/assetpack/src/manifest/pixiManifest.ts @@ -1,5 +1,5 @@ import fs from 'fs-extra'; -import { path, stripTags } from '../core/index.js'; +import { Logger, path, stripTags } from '../core/index.js'; import type { Asset, @@ -11,6 +11,7 @@ export interface PixiBundle { name: string; assets: PixiManifestEntry[]; + relativeName?: string; } export interface PixiManifest @@ -46,6 +47,11 @@ export interface PixiManifestOptions extends PluginOptions * if true, the metaData will be outputted in the data field of the manifest. */ includeMetaData?: boolean; + /** + * The name style for asset bundles in the manifest file. + * When set to relative, asset bundles will use their relative paths as names. + */ + nameStyle?: 'short' | 'relative'; /** * if true, the all tags will be outputted in the data.tags field of the manifest. * If false, only internal tags will be outputted to the data.tags field. All other tags will be outputted to the data field directly. @@ -85,6 +91,7 @@ export function pixiManifest(_options: PixiManifestOptions = {}): AssetPipe(); + const isNameStyleShort = options.nameStyle !== 'relative'; + const bundleNames = new Set(); + const duplicateBundleNames = new Set(); manifest.bundles.forEach((bundle) => - bundle.assets.forEach((asset) => nameMap.set(asset, asset.alias as string[]))); + { + if (isNameStyleShort) + { + if (bundleNames.has(bundle.name)) + { + duplicateBundleNames.add(bundle.name); + Logger.warn(`[AssetPack][manifest] Duplicate bundle name '${bundle.name}'. All bundles with that name will be renamed to their relative name instead.`); + } + else + { + bundleNames.add(bundle.name); + } + } + + bundle.assets.forEach((asset) => nameMap.set(asset, asset.alias as string[])); + }); const arrays = Array.from(nameMap.values()); const sets = arrays.map((arr) => new Set(arr)); @@ -134,6 +159,15 @@ function filterUniqueNames(manifest: PixiManifest) manifest.bundles.forEach((bundle) => { + if (isNameStyleShort) + { + // Switch to relative bundle name to avoid duplications + if (duplicateBundleNames.has(bundle.name)) + { + bundle.name = bundle.relativeName ?? bundle.name; + } + } + bundle.assets.forEach((asset) => { const names = nameMap.get(asset) as string[]; @@ -143,6 +177,21 @@ function filterUniqueNames(manifest: PixiManifest) }); } +function getRelativeBundleName(asset: Asset, entryPath: string): string +{ + let name = asset.filename; + let parent = asset.parent; + + // Exclude assets the paths of which equal to the entry path + while (parent && parent.path !== entryPath) + { + name = `${parent.filename}/${name}`; + parent = parent.parent; + } + + return stripTags(name); +} + function collectAssets( asset: Asset, options: PixiManifestOptions, @@ -163,10 +212,23 @@ function collectAssets( if (asset.metaData[tags!.manifest!]) { localBundle = { - name: stripTags(asset.filename), + name: options.nameStyle === 'relative' ? getRelativeBundleName(asset, entryPath) : stripTags(asset.filename), assets: [] }; + // This property helps rename duplicate bundle declarations + // Also, mark it as non-enumerable to prevent fs from including it into output + if (options.nameStyle !== 'relative') + { + Object.defineProperty(localBundle, 'relativeName', { + enumerable: false, + get() + { + return getRelativeBundleName(asset, entryPath); + } + }); + } + bundles.push(localBundle); } diff --git a/packages/assetpack/src/webfont/webfont.ts b/packages/assetpack/src/webfont/webfont.ts index 4ed24712..92af36cd 100644 --- a/packages/assetpack/src/webfont/webfont.ts +++ b/packages/assetpack/src/webfont/webfont.ts @@ -1,4 +1,4 @@ -import { checkExt, createNewAssetAt, path } from '../core/index.js'; +import { checkExt, createNewAssetAt, path, stripTags } from '../core/index.js'; import { fonts } from './fonts.js'; import type { Asset, AssetPipe } from '../core/index.js'; @@ -44,7 +44,7 @@ export function webfont(): AssetPipe newAsset.buffer = buffer; // set the family name to the filename if it doesn't exist - asset.metaData.family ??= path.trimExt(asset.filename); + asset.metaData.family ??= stripTags(path.trimExt(asset.filename)); return [newAsset]; } diff --git a/packages/assetpack/test/core/AssetCache.test.ts b/packages/assetpack/test/core/AssetCache.test.ts index 53a0c38e..f510e626 100644 --- a/packages/assetpack/test/core/AssetCache.test.ts +++ b/packages/assetpack/test/core/AssetCache.test.ts @@ -37,6 +37,7 @@ describe('AssetCache', () => expect(cachedAssetData).toEqual({ test: { + inheritedMetaData: {}, isFolder: true, metaData: {}, transformData: {}, @@ -44,9 +45,10 @@ describe('AssetCache', () => 'test/test.json': { isFolder: false, hash: '12345', + inheritedMetaData: {}, parent: 'test', transformData: {}, - metaData: {} + metaData: {}, } }); }); diff --git a/packages/assetpack/test/json/Json.test.ts b/packages/assetpack/test/json/Json.test.ts index a367b82d..cd5ea9dc 100644 --- a/packages/assetpack/test/json/Json.test.ts +++ b/packages/assetpack/test/json/Json.test.ts @@ -1,3 +1,4 @@ +import { readJSONSync } from 'fs-extra'; import { existsSync, readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { AssetPack } from '../../src/core/index.js'; @@ -124,4 +125,46 @@ describe('Json', () => expect(data.replace(/\\/g, '').trim()).toEqual(`{"hello":"world","Im":"not broken"}`); }); + + it('should support json5 format', async () => + { + const testName = 'json5'; + const inputDir = getInputDir(pkg, testName); + const outputDir = getOutputDir(pkg, testName); + + createFolder( + pkg, + { + name: testName, + files: [ + { + name: 'json5.json', + content: assetPath('json/json5.json'), + }, + { + name: 'other-json-5.json5', + content: assetPath('json/json5.json'), + }, + ], + folders: [], + } + ); + + const assetpack = new AssetPack({ + entry: inputDir, cacheLocation: getCacheDir(pkg, testName), + output: outputDir, + cache: false, + pipes: [ + json() + ] + }); + + await assetpack.run(); + + const json5Data = readJSONSync(`${outputDir}/json5.json`, 'utf8'); + + expect(json5Data).toEqual({ hello: 'world', Im: 'not broken' }); + + expect(existsSync(`${outputDir}/other-json-5.json`)).toBe(true); + }); }); diff --git a/packages/assetpack/test/manifest/Manifest.test.ts b/packages/assetpack/test/manifest/Manifest.test.ts index d4ca29d1..826c229c 100644 --- a/packages/assetpack/test/manifest/Manifest.test.ts +++ b/packages/assetpack/test/manifest/Manifest.test.ts @@ -1038,6 +1038,236 @@ describe('Manifest', () => }); }); + it('should ensure sub-manifests are created correctly with short names', async () => + { + const testName = 'manifest-sub-manifest-short'; + const inputDir = getInputDir(pkg, testName); + const outputDir = getOutputDir(pkg, testName); + + createFolder(pkg, { + name: testName, + files: [], + folders: [ + { + name: 'sound{m}', + files: [ + { + name: '1.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [], + }, + { + name: 'sound2{m}', + files: [ + { + name: '2.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [], + }, + { + name: 'sound3{m}', + files: [ + { + name: '3.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [ + { + name: 'sound2{m}', + files: [ + { + name: '2.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [], + }, + ], + }, + ], + }); + + const assetpack = new AssetPack({ + entry: inputDir, + cacheLocation: getCacheDir(pkg, testName), + output: outputDir, + cache: false, + pipes: [ + pixiManifest({ + includeMetaData: false, + }), + ], + }); + + await assetpack.run(); + + expect(fs.readJSONSync(`${outputDir}/manifest.json`)).toEqual({ + bundles: [ + { + name: 'default', + assets: [], + }, + { + name: 'sound2', + assets: [ + { + alias: ['sound2/2.mp3'], + src: ['sound2/2.mp3'], + }, + ], + }, + { + name: 'sound3', + assets: [ + { + alias: ['sound3/3.mp3'], + src: ['sound3/3.mp3'], + }, + ], + }, + { + name: 'sound3/sound2', + assets: [ + { + alias: ['sound3/sound2/2.mp3'], + src: ['sound3/sound2/2.mp3'], + }, + ], + }, + { + name: 'sound', + assets: [ + { + alias: ['sound/1.mp3'], + src: ['sound/1.mp3'], + }, + ], + }, + ], + }); + }); + + it('should ensure sub-manifests are created correctly with relative names', async () => + { + const testName = 'manifest-sub-manifest-relative'; + const inputDir = getInputDir(pkg, testName); + const outputDir = getOutputDir(pkg, testName); + + createFolder(pkg, { + name: testName, + files: [], + folders: [ + { + name: 'sound{m}', + files: [ + { + name: '1.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [ + { + name: 'sound2{m}', + files: [ + { + name: '2.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [], + }, + ], + }, + { + name: 'sound3{m}', + files: [ + { + name: '3.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [ + { + name: 'sound2{m}', + files: [ + { + name: '2.mp3', + content: assetPath('audio/1.mp3'), + }, + ], + folders: [], + }, + ], + }, + ], + }); + + const assetpack = new AssetPack({ + entry: inputDir, + cacheLocation: getCacheDir(pkg, testName), + output: outputDir, + cache: false, + pipes: [ + pixiManifest({ + includeMetaData: false, + nameStyle: 'relative', + }), + ], + }); + + await assetpack.run(); + + expect(fs.readJSONSync(`${outputDir}/manifest.json`)).toEqual({ + bundles: [ + { + name: 'default', + assets: [], + }, + { + name: 'sound3', + assets: [ + { + alias: ['sound3/3.mp3'], + src: ['sound3/3.mp3'], + }, + ], + }, + { + name: 'sound3/sound2', + assets: [ + { + alias: ['sound3/sound2/2.mp3'], + src: ['sound3/sound2/2.mp3'], + }, + ], + }, + { + name: 'sound', + assets: [ + { + alias: ['sound/1.mp3'], + src: ['sound/1.mp3'], + }, + ], + }, + { + name: 'sound/sound2', + assets: [ + { + alias: ['sound/sound2/2.mp3'], + src: ['sound/sound2/2.mp3'], + }, + ], + }, + ], + }); + }); + it('should ignore files with the mIgnore tag', async () => { const testName = 'manifest-ignore'; diff --git a/packages/assetpack/test/resources/json/json5.json b/packages/assetpack/test/resources/json/json5.json new file mode 100644 index 00000000..d7dc470c --- /dev/null +++ b/packages/assetpack/test/resources/json/json5.json @@ -0,0 +1,5 @@ +{ + // so cool support for comments + hello: "world", + Im: "not broken" +} diff --git a/packages/assetpack/test/webfont/Webfont.test.ts b/packages/assetpack/test/webfont/Webfont.test.ts index ea7e6957..856654b8 100644 --- a/packages/assetpack/test/webfont/Webfont.test.ts +++ b/packages/assetpack/test/webfont/Webfont.test.ts @@ -241,10 +241,10 @@ describe('Webfont', () => files: [], folders: [ { - name: 'defaultFolder{wf}', + name: 'defaultFolder', files: [ { - name: 'ttf.ttf', + name: 'ttf{wf}.ttf', content: assetPath('font/Roboto-Regular.ttf'), }, ], diff --git a/packages/docs/docs/guide/pipes/manifest.mdx b/packages/docs/docs/guide/pipes/manifest.mdx index 81338679..d0f60e82 100644 --- a/packages/docs/docs/guide/pipes/manifest.mdx +++ b/packages/docs/docs/guide/pipes/manifest.mdx @@ -40,6 +40,7 @@ export default { createShortcuts: false, trimExtensions: false, includeMetaData: true, + nameStyle: 'short' }) ], }; @@ -47,12 +48,13 @@ export default { ## API -| Option | Type | Description | -| --------------- | --------- | ---------------------------------------------------------------------------------------------- | -| output | `string` | The path to the manifest file.
Defaults to the output folder defined in your config. | -| createShortcuts | `boolean` | Whether to create the shortest possible alias for an asset.
Defaults to `false`. | -| trimExtensions | `boolean` | Whether to trim the extensions from the asset aliases.
Defaults to `false`. | -| includeMetaData | `boolean` | Whether to include the tags the asset has used in the manifest file.
Defaults to `true`. | +| Option | Type | Description | +| --------------- | ----------------- | ---------------------------------------------------------------------------------------------- | +| output | `string` | The path to the manifest file.
Defaults to the output folder defined in your config. | +| createShortcuts | `boolean` | Whether to create the shortest possible alias for an asset.
Defaults to `false`. | +| trimExtensions | `boolean` | Whether to trim the extensions from the asset aliases.
Defaults to `false`. | +| includeMetaData | `boolean` | Whether to include the tags the asset has used in the manifest file.
Defaults to `true`. | +| nameStyle | `short\|relative` | The name style for the bundle names in the manifest file.
Defaults to `short`. | ## Tags