Skip to content

Commit

Permalink
Reduce usage of long paths in assets which can cause long path issues (
Browse files Browse the repository at this point in the history
…#12942)

* Handling for long asset paths

* Change files

* fix lint

* fix

---------

Co-authored-by: Marlene Cota <mcota@microsoft.com>
  • Loading branch information
acoates-ms and marlenecota authored Apr 16, 2024
1 parent 8628f20 commit 1dc39a4
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Handling for long asset paths",
"packageName": "react-native-windows",
"email": "30809111+acoates-ms@users.noreply.github.com",
"dependentChangeType": "patch"
}
15 changes: 15 additions & 0 deletions vnext/metroShortPathAssetDataPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @ts-check
/**
* @typedef {import("metro").AssetData} AssetData;
**/

/**
* @param {AssetData & {__useShortPath: boolean}} asset
* @returns {Promise<AssetData>}
*/
async function metroShortPathAssetDataPlugin(asset) {
asset.__useShortPath = true;
return Promise.resolve(asset);
}

module.exports = metroShortPathAssetDataPlugin;
10 changes: 10 additions & 0 deletions vnext/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,16 @@
"baseFile": "packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.android.js",
"baseHash": "1c9eb481e8ed077fa650e3ea34837e2a31310366"
},
{
"type": "platform",
"file": "src-win/Libraries/Image/assetPaths.js"
},
{
"type": "derived",
"file": "src-win/Libraries/Image/AssetSourceResolver.windows.js",
"baseFile": "packages/react-native/Libraries/Image/AssetSourceResolver.js",
"baseHash": "d9d768a3b1d5cd394e63e8bf68456e1e6ca7f5d2"
},
{
"type": "patch",
"file": "src-win/Libraries/Image/Image.windows.js",
Expand Down
45 changes: 45 additions & 0 deletions vnext/saveAssetPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// @ts-check
const path = require('path');
const ensureShortPath = require('./Libraries/Image/assetPaths');

/**
* @typedef {import("metro").AssetData} AssetData;
**/

/**
* @param {AssetData} asset
* @param {number} scale
* @returns {string}
*/
function getAssetDestPath(asset, scale) {
const suffix = scale === 1 ? '' : `@${scale}x`;
const fileName = `${asset.name + suffix}.${asset.type}`;
return path.join(
// Assets can have relative paths outside of the project root.
// Replace `../` with `_` to make sure they don't end up outside of
// the expected assets directory.
ensureShortPath(asset.httpServerLocation.substr(1).replace(/\.\.\//g, '_')),
fileName,
);
}

/**
* @param {ReadonlyArray<AssetData>} assets
* @param {string} _platform
* @param {string | undefined} _assetsDest
* @param {string | undefined} _assetCatalogDest
* @param {(asset: AssetData, allowedScales: number[] | undefined, getAssetDestPath: (asset: AssetData, scale: number) => string) => void} addAssetToCopy
*/
function saveAssetsWin32(
assets,
_platform,
_assetsDest,
_assetCatalogDest,
addAssetToCopy,
) {
assets.forEach((asset) =>
addAssetToCopy(asset, undefined, getAssetDestPath),
);
}

module.exports = saveAssetsWin32;
186 changes: 186 additions & 0 deletions vnext/src-win/Libraries/Image/AssetSourceResolver.windows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

export type ResolvedAssetSource = {|
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
|};

import type {PackagerAsset} from '@react-native/assets-registry/registry';

const PixelRatio = require('../Utilities/PixelRatio').default;
const Platform = require('../Utilities/Platform');
const {pickScale} = require('./AssetUtils');
const {
getAndroidResourceFolderName,
getAndroidResourceIdentifier,
} = require('@react-native/assets-registry/path-support');
const invariant = require('invariant');
// $FlowFixMe[untyped-import]
const ensureShortPath = require('./assetPaths.js'); // [Windows]

// [Windows - instead of using basePath from @react-native/assets-registry/path-support]
function getBasePath(asset: PackagerAsset, local: boolean) {
let basePath = asset.httpServerLocation;
if (basePath[0] === '/') {
basePath = basePath.substr(1);
}

if (local) {
const safePath = basePath.replace(/\.\.\//g, '_');
// If this asset was created with saveAssetPlugin, then we should shorten the path
// This conditional allow compat of bundles which might have been created without the saveAssetPlugin
// $FlowFixMe: __useShortPath not part of public type
if (asset.__useShortPath) {
return ensureShortPath(safePath);
}
return safePath;
}

return basePath;
}

/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset: PackagerAsset, local: boolean): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const assetDir = getBasePath(asset, local);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}

/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset: PackagerAsset): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const drawableFolder = getAndroidResourceFolderName(asset, scale);
const fileName = getAndroidResourceIdentifier(asset);
return drawableFolder + '/' + fileName + '.' + asset.type;
}

class AssetSourceResolver {
serverUrl: ?string;
// where the jsbundle is being run from
jsbundleUrl: ?string;
// the asset to resolve
asset: PackagerAsset;

constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.jsbundleUrl = jsbundleUrl;
this.asset = asset;
}

isLoadedFromServer(): boolean {
return !!this.serverUrl;
}

isLoadedFromFileSystem(): boolean {
return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://');
}

defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}

if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}

/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(this.serverUrl != null, 'need server to load from');
return this.fromSource(
this.serverUrl +
getScaledAssetPath(this.asset, false) +
'?platform=' +
Platform.OS +
'&hash=' +
this.asset.hash,
);
}

/**
* Resolves to just the scaled asset filename
* E.g. 'assets/AwesomeModule/icon@2x.png'
*/
scaledAssetPath(local: boolean): ResolvedAssetSource {
return this.fromSource(getScaledAssetPath(this.asset, local));
}

/**
* Resolves to where the bundle is running from, with a scaled asset filename
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/icon@2x.png'
*/
scaledAssetURLNearBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl ?? 'file://';
return this.fromSource(
// Assets can have relative paths outside of the project root.
// When bundling them we replace `../` with `_` to make sure they
// don't end up outside of the expected assets directory.
path + getScaledAssetPath(this.asset, true).replace(/\.\.\//g, '_'),
);
}

/**
* The default location of assets bundled with the app, located by
* resource identifier
* The Android resource system picks the correct scale.
* E.g. 'assets_awesomemodule_icon'
*/
resourceIdentifierWithoutScale(): ResolvedAssetSource {
invariant(
Platform.OS === 'android',
'resource identifiers work on Android',
);
return this.fromSource(getAndroidResourceIdentifier(this.asset));
}

/**
* If the jsbundle is running from a sideload location, this resolves assets
* relative to its location
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
*/
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl ?? 'file://';
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
}

fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width,
height: this.asset.height,
uri: source,
scale: pickScale(this.asset.scales, PixelRatio.get()),
};
}

static pickScale: (scales: Array<number>, deviceScale?: number) => number =
pickScale;
}

module.exports = AssetSourceResolver;
36 changes: 36 additions & 0 deletions vnext/src-win/Libraries/Image/assetPaths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*
* Dont use flow here, since this file is used by saveAssetPlugin.js which will run without flow transform
* @format
*/

'use strict';

// Some windows machines may not have long paths enabled
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
// Assets in nested node_modules (common when using pnpm) - end up creating very long paths
// Using this function we shorten longer paths to prevent paths from hitting the path limit
function ensureShortPath(str) {
if (str.length < 40) return str;

const assetsPrefix = 'assets/';

if (!str.startsWith(assetsPrefix)) {
console.warn(`Unexpected asset uri - ${str} may not load correctly.`);
}

const postStr = str.slice(assetsPrefix.length);
var hash = 0,
i,
chr;
for (i = 0; i < postStr.length; i++) {
chr = postStr.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return assetsPrefix + hash.toString();
}

module.exports = ensureShortPath;

0 comments on commit 1dc39a4

Please sign in to comment.