Skip to content

Commit 1dc39a4

Browse files
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 <[email protected]>
1 parent 8628f20 commit 1dc39a4

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Handling for long asset paths",
4+
"packageName": "react-native-windows",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @ts-check
2+
/**
3+
* @typedef {import("metro").AssetData} AssetData;
4+
**/
5+
6+
/**
7+
* @param {AssetData & {__useShortPath: boolean}} asset
8+
* @returns {Promise<AssetData>}
9+
*/
10+
async function metroShortPathAssetDataPlugin(asset) {
11+
asset.__useShortPath = true;
12+
return Promise.resolve(asset);
13+
}
14+
15+
module.exports = metroShortPathAssetDataPlugin;

vnext/overrides.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@
377377
"baseFile": "packages/react-native/Libraries/DevToolsSettings/DevToolsSettingsManager.android.js",
378378
"baseHash": "1c9eb481e8ed077fa650e3ea34837e2a31310366"
379379
},
380+
{
381+
"type": "platform",
382+
"file": "src-win/Libraries/Image/assetPaths.js"
383+
},
384+
{
385+
"type": "derived",
386+
"file": "src-win/Libraries/Image/AssetSourceResolver.windows.js",
387+
"baseFile": "packages/react-native/Libraries/Image/AssetSourceResolver.js",
388+
"baseHash": "d9d768a3b1d5cd394e63e8bf68456e1e6ca7f5d2"
389+
},
380390
{
381391
"type": "patch",
382392
"file": "src-win/Libraries/Image/Image.windows.js",

vnext/saveAssetPlugin.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// @ts-check
2+
const path = require('path');
3+
const ensureShortPath = require('./Libraries/Image/assetPaths');
4+
5+
/**
6+
* @typedef {import("metro").AssetData} AssetData;
7+
**/
8+
9+
/**
10+
* @param {AssetData} asset
11+
* @param {number} scale
12+
* @returns {string}
13+
*/
14+
function getAssetDestPath(asset, scale) {
15+
const suffix = scale === 1 ? '' : `@${scale}x`;
16+
const fileName = `${asset.name + suffix}.${asset.type}`;
17+
return path.join(
18+
// Assets can have relative paths outside of the project root.
19+
// Replace `../` with `_` to make sure they don't end up outside of
20+
// the expected assets directory.
21+
ensureShortPath(asset.httpServerLocation.substr(1).replace(/\.\.\//g, '_')),
22+
fileName,
23+
);
24+
}
25+
26+
/**
27+
* @param {ReadonlyArray<AssetData>} assets
28+
* @param {string} _platform
29+
* @param {string | undefined} _assetsDest
30+
* @param {string | undefined} _assetCatalogDest
31+
* @param {(asset: AssetData, allowedScales: number[] | undefined, getAssetDestPath: (asset: AssetData, scale: number) => string) => void} addAssetToCopy
32+
*/
33+
function saveAssetsWin32(
34+
assets,
35+
_platform,
36+
_assetsDest,
37+
_assetCatalogDest,
38+
addAssetToCopy,
39+
) {
40+
assets.forEach((asset) =>
41+
addAssetToCopy(asset, undefined, getAssetDestPath),
42+
);
43+
}
44+
45+
module.exports = saveAssetsWin32;
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
export type ResolvedAssetSource = {|
14+
+__packager_asset: boolean,
15+
+width: ?number,
16+
+height: ?number,
17+
+uri: string,
18+
+scale: number,
19+
|};
20+
21+
import type {PackagerAsset} from '@react-native/assets-registry/registry';
22+
23+
const PixelRatio = require('../Utilities/PixelRatio').default;
24+
const Platform = require('../Utilities/Platform');
25+
const {pickScale} = require('./AssetUtils');
26+
const {
27+
getAndroidResourceFolderName,
28+
getAndroidResourceIdentifier,
29+
} = require('@react-native/assets-registry/path-support');
30+
const invariant = require('invariant');
31+
// $FlowFixMe[untyped-import]
32+
const ensureShortPath = require('./assetPaths.js'); // [Windows]
33+
34+
// [Windows - instead of using basePath from @react-native/assets-registry/path-support]
35+
function getBasePath(asset: PackagerAsset, local: boolean) {
36+
let basePath = asset.httpServerLocation;
37+
if (basePath[0] === '/') {
38+
basePath = basePath.substr(1);
39+
}
40+
41+
if (local) {
42+
const safePath = basePath.replace(/\.\.\//g, '_');
43+
// If this asset was created with saveAssetPlugin, then we should shorten the path
44+
// This conditional allow compat of bundles which might have been created without the saveAssetPlugin
45+
// $FlowFixMe: __useShortPath not part of public type
46+
if (asset.__useShortPath) {
47+
return ensureShortPath(safePath);
48+
}
49+
return safePath;
50+
}
51+
52+
return basePath;
53+
}
54+
55+
/**
56+
* Returns a path like 'assets/AwesomeModule/[email protected]'
57+
*/
58+
function getScaledAssetPath(asset: PackagerAsset, local: boolean): string {
59+
const scale = pickScale(asset.scales, PixelRatio.get());
60+
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
61+
const assetDir = getBasePath(asset, local);
62+
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
63+
}
64+
65+
/**
66+
* Returns a path like 'drawable-mdpi/icon.png'
67+
*/
68+
function getAssetPathInDrawableFolder(asset: PackagerAsset): string {
69+
const scale = pickScale(asset.scales, PixelRatio.get());
70+
const drawableFolder = getAndroidResourceFolderName(asset, scale);
71+
const fileName = getAndroidResourceIdentifier(asset);
72+
return drawableFolder + '/' + fileName + '.' + asset.type;
73+
}
74+
75+
class AssetSourceResolver {
76+
serverUrl: ?string;
77+
// where the jsbundle is being run from
78+
jsbundleUrl: ?string;
79+
// the asset to resolve
80+
asset: PackagerAsset;
81+
82+
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
83+
this.serverUrl = serverUrl;
84+
this.jsbundleUrl = jsbundleUrl;
85+
this.asset = asset;
86+
}
87+
88+
isLoadedFromServer(): boolean {
89+
return !!this.serverUrl;
90+
}
91+
92+
isLoadedFromFileSystem(): boolean {
93+
return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://');
94+
}
95+
96+
defaultAsset(): ResolvedAssetSource {
97+
if (this.isLoadedFromServer()) {
98+
return this.assetServerURL();
99+
}
100+
101+
if (Platform.OS === 'android') {
102+
return this.isLoadedFromFileSystem()
103+
? this.drawableFolderInBundle()
104+
: this.resourceIdentifierWithoutScale();
105+
} else {
106+
return this.scaledAssetURLNearBundle();
107+
}
108+
}
109+
110+
/**
111+
* Returns an absolute URL which can be used to fetch the asset
112+
* from the devserver
113+
*/
114+
assetServerURL(): ResolvedAssetSource {
115+
invariant(this.serverUrl != null, 'need server to load from');
116+
return this.fromSource(
117+
this.serverUrl +
118+
getScaledAssetPath(this.asset, false) +
119+
'?platform=' +
120+
Platform.OS +
121+
'&hash=' +
122+
this.asset.hash,
123+
);
124+
}
125+
126+
/**
127+
* Resolves to just the scaled asset filename
128+
* E.g. 'assets/AwesomeModule/[email protected]'
129+
*/
130+
scaledAssetPath(local: boolean): ResolvedAssetSource {
131+
return this.fromSource(getScaledAssetPath(this.asset, local));
132+
}
133+
134+
/**
135+
* Resolves to where the bundle is running from, with a scaled asset filename
136+
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/[email protected]'
137+
*/
138+
scaledAssetURLNearBundle(): ResolvedAssetSource {
139+
const path = this.jsbundleUrl ?? 'file://';
140+
return this.fromSource(
141+
// Assets can have relative paths outside of the project root.
142+
// When bundling them we replace `../` with `_` to make sure they
143+
// don't end up outside of the expected assets directory.
144+
path + getScaledAssetPath(this.asset, true).replace(/\.\.\//g, '_'),
145+
);
146+
}
147+
148+
/**
149+
* The default location of assets bundled with the app, located by
150+
* resource identifier
151+
* The Android resource system picks the correct scale.
152+
* E.g. 'assets_awesomemodule_icon'
153+
*/
154+
resourceIdentifierWithoutScale(): ResolvedAssetSource {
155+
invariant(
156+
Platform.OS === 'android',
157+
'resource identifiers work on Android',
158+
);
159+
return this.fromSource(getAndroidResourceIdentifier(this.asset));
160+
}
161+
162+
/**
163+
* If the jsbundle is running from a sideload location, this resolves assets
164+
* relative to its location
165+
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
166+
*/
167+
drawableFolderInBundle(): ResolvedAssetSource {
168+
const path = this.jsbundleUrl ?? 'file://';
169+
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
170+
}
171+
172+
fromSource(source: string): ResolvedAssetSource {
173+
return {
174+
__packager_asset: true,
175+
width: this.asset.width,
176+
height: this.asset.height,
177+
uri: source,
178+
scale: pickScale(this.asset.scales, PixelRatio.get()),
179+
};
180+
}
181+
182+
static pickScale: (scales: Array<number>, deviceScale?: number) => number =
183+
pickScale;
184+
}
185+
186+
module.exports = AssetSourceResolver;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
*
5+
* Dont use flow here, since this file is used by saveAssetPlugin.js which will run without flow transform
6+
* @format
7+
*/
8+
9+
'use strict';
10+
11+
// Some windows machines may not have long paths enabled
12+
// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
13+
// Assets in nested node_modules (common when using pnpm) - end up creating very long paths
14+
// Using this function we shorten longer paths to prevent paths from hitting the path limit
15+
function ensureShortPath(str) {
16+
if (str.length < 40) return str;
17+
18+
const assetsPrefix = 'assets/';
19+
20+
if (!str.startsWith(assetsPrefix)) {
21+
console.warn(`Unexpected asset uri - ${str} may not load correctly.`);
22+
}
23+
24+
const postStr = str.slice(assetsPrefix.length);
25+
var hash = 0,
26+
i,
27+
chr;
28+
for (i = 0; i < postStr.length; i++) {
29+
chr = postStr.charCodeAt(i);
30+
hash = (hash << 5) - hash + chr;
31+
hash |= 0; // Convert to 32bit integer
32+
}
33+
return assetsPrefix + hash.toString();
34+
}
35+
36+
module.exports = ensureShortPath;

0 commit comments

Comments
 (0)