-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Reduce usage of long paths in assets which can cause long path issues (…
…#12942) * Handling for long asset paths * Change files * fix lint * fix --------- Co-authored-by: Marlene Cota <mcota@microsoft.com>
- Loading branch information
1 parent
8628f20
commit 1dc39a4
Showing
6 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/react-native-windows-cb387bc4-0e9f-418b-bbf5-dc96833f9921.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
186
vnext/src-win/Libraries/Image/AssetSourceResolver.windows.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |