Skip to content

Commit

Permalink
fix(expo, asset, cli)!: unify asset hashing (#24090)
Browse files Browse the repository at this point in the history
# Why

Convert the `../` segments of the server URL to `_` to support
monorepos. This same transformation takes place in
`AssetSourceResolver.web` (expo-assets, expo-image) and
`persistMetroAssets` of Expo CLI, this originally came from the [Metro
opinion](https://github.com/react-native-community/cli/blob/2204d357379e2067cebe2791e90388f7e97fc5f5/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts#L19C5-L19C10).

The purpose is to ensure no URL like `/foo/../bar.png` is requested, as
the result would be `/bar.png` which wouldn't work. The of how to handle
this didn't come from us, but we do need to ensure it's unified. At a
high-level, this does prevent the usage of certain files, as
`/foo/../bar.png` will be `/foo/_bar.png`, meaning a file named
`/foo/_bar.png` cannot also exist. This logic, while applied at runtime,
is actually only valid for production exports as we don't move or alias
files in development. The only way to have valid development files is to
ensure `../` never appears in the URL, i.e. by using
`unstable_serverRoot`.

- Drop legacy `expo/tools/hashAssetFiles.js` in favor of `expo-asset`
version.
- Unify runtime logic of asset file loading for monorepos.
- Split out of #23911

<!--
Please describe the motivation for this PR, and link to relevant GitHub
issues, forums posts, or feature requests.
-->

# How

<!--
How did you build this feature or fix this bug and why?
-->

# Test Plan

<!--
Please describe how you tested this change and how a reviewer could
reproduce your test, especially if this PR does not include automated
tests! If possible, please also provide terminal output and/or
screenshots demonstrating your test/reproduction.
-->

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [ ] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).
  • Loading branch information
EvanBacon authored Aug 26, 2023
1 parent a7490a1 commit 429dc7f
Show file tree
Hide file tree
Showing 25 changed files with 217 additions and 153 deletions.
10 changes: 10 additions & 0 deletions apps/router-e2e/__e2e__/static-rendering/app/asset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Image } from 'react-native';

export default function Page() {
return (
<>
<Image source={require('../../../assets/icon.png')} />
<Image source={require('expo-router/assets/file.png')} />
</>
);
}
2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Support monorepo assets with `npx expo export:embed`. ([#24095](https://github.com/expo/expo/pull/24095) by [@EvanBacon](https://github.com/EvanBacon))
- Forward exact Metro server error during static rendering. ([#23909](https://github.com/expo/expo/pull/23909) by [@EvanBacon](https://github.com/EvanBacon))
- Add **Server** tag when bundling for server environments. ([#23987](https://github.com/expo/expo/pull/23987) by [@EvanBacon](https://github.com/EvanBacon))
- Support importing assets from out of the project root when working in monorepos on web. ([#24090](https://github.com/expo/expo/pull/24090) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

Expand All @@ -23,6 +24,7 @@

### 💡 Others

- Remove unused `md5-file` library. ([#24090](https://github.com/expo/expo/pull/24090) by [@EvanBacon](https://github.com/EvanBacon))
- Remove unused `--generate-static-view-configs` flag from `npx expo export:embed`. ([#24095](https://github.com/expo/expo/pull/24095) by [@EvanBacon](https://github.com/EvanBacon))
- Fix types and CI. ([#24051](https://github.com/expo/expo/pull/24051) by [@EvanBacon](https://github.com/EvanBacon))
- Update Expo Router tests. ([#23795](https://github.com/expo/expo/pull/23795) by [@EvanBacon](https://github.com/EvanBacon))
Expand Down
1 change: 1 addition & 0 deletions packages/@expo/cli/e2e/__tests__/export-embed-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ it(
'assets/__packages/expo-router/assets/file.png',
'assets/__packages/expo-router/assets/forward.png',
'assets/__packages/expo-router/assets/pkg.png',
'assets/assets/icon.png',
'output.js',
]);
},
Expand Down
1 change: 0 additions & 1 deletion packages/@expo/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
"is-root": "^2.1.0",
"js-yaml": "^3.13.1",
"json-schema-deref-sync": "^0.13.0",
"md5-file": "^3.2.3",
"md5hex": "^1.0.0",
"minipass": "3.1.6",
"node-fetch": "^2.6.7",
Expand Down
16 changes: 11 additions & 5 deletions packages/@expo/cli/src/export/embed/exportEmbedAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ export async function exportEmbedAsync(projectRoot: string, options: Options) {
setNodeEnv(options.dev ? 'development' : 'production');
require('@expo/env').load(projectRoot);

const { config } = await loadMetroConfigAsync(projectRoot, {
maxWorkers: options.maxWorkers,
resetCache: options.resetCache,
config: options.config,
});
const { config } = await loadMetroConfigAsync(
projectRoot,
{
maxWorkers: options.maxWorkers,
resetCache: options.resetCache,
config: options.config,
},
{
isExporting: true,
}
);

const saveAssets = importCliSaveAssetsFromProject(projectRoot);

Expand Down
4 changes: 4 additions & 0 deletions packages/@expo/cli/src/export/exportStaticAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export async function unstable_exportStaticAsync(projectRoot: string, options: O
await devServerManager.startAsync([
{
type: 'metro',
options: {
location: {},
isExporting: true,
},
},
]);

Expand Down
1 change: 1 addition & 0 deletions packages/@expo/cli/src/export/fork-bundleAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export async function bundleAsync(

const { config, reporter } = await loadMetroConfigAsync(projectRoot, options, {
exp: expoConfig,
isExporting: true,
});

const metroServer = await metro.runMetro(config, {
Expand Down
3 changes: 3 additions & 0 deletions packages/@expo/cli/src/start/server/BundlerDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export interface BundlerStartOptions {
/** Should instruct the bundler to create minified bundles. */
minify?: boolean;

/** Will the bundler be used for exporting. NOTE: This is an odd option to pass to the dev server. */
isExporting?: boolean;

// Webpack options
/** Should modify and create PWA icons. */
isImageEditingEnabled?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,10 @@ export class MetroBundlerDevServer extends BundlerDevServer {

const { metro, server, middleware, messageSocket } = await instantiateMetroAsync(
this,
parsedOptions
parsedOptions,
{
isExporting: !!options.isExporting,
}
);

const manifestMiddleware = await this.getManifestMiddlewareAsync(options);
Expand Down
58 changes: 54 additions & 4 deletions packages/@expo/cli/src/start/server/metro/instantiateMetro.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ExpoConfig, getConfig } from '@expo/config';
import { MetroDevServerOptions } from '@expo/dev-server';
import { MetroDevServerOptions, prependMiddleware } from '@expo/dev-server';
import type { LoadOptions } from '@expo/metro-config';
import chalk from 'chalk';
import http from 'http';
import type Metro from 'metro';
import { Terminal } from 'metro-core';
import semver from 'semver';
import { URL } from 'url';

import { MetroBundlerDevServer } from './MetroBundlerDevServer';
import { MetroTerminalReporter } from './MetroTerminalReporter';
Expand All @@ -19,19 +21,37 @@ import { logEventAsync } from '../../../utils/analytics/rudderstackClient';
import { env } from '../../../utils/env';
import { getMetroServerRoot } from '../middleware/ManifestMiddleware';
import { createDevServerMiddleware } from '../middleware/createDevServerMiddleware';
import { ServerNext, ServerRequest, ServerResponse } from '../middleware/server.types';
import { getPlatformBundlers } from '../platformBundlers';

// From expo/dev-server but with ability to use custom logger.
type MessageSocket = {
broadcast: (method: string, params?: Record<string, any> | undefined) => void;
};

function gteSdkVersion(exp: Pick<ExpoConfig, 'sdkVersion'>, sdkVersion: string): boolean {
if (!exp.sdkVersion) {
return false;
}

if (exp.sdkVersion === 'UNVERSIONED') {
return true;
}

try {
return semver.gte(exp.sdkVersion, sdkVersion);
} catch {
throw new Error(`${exp.sdkVersion} is not a valid version. Must be in the form of x.y.z`);
}
}

export async function loadMetroConfigAsync(
projectRoot: string,
options: LoadOptions,
{
exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp,
}: { exp?: ExpoConfig } = {}
isExporting,
}: { exp?: ExpoConfig; isExporting: boolean }
) {
let reportEvent: ((event: any) => void) | undefined;
const serverRoot = getMetroServerRoot(projectRoot);
Expand All @@ -51,6 +71,22 @@ export async function loadMetroConfigAsync(
const ExpoMetroConfig = importExpoMetroConfig(projectRoot);
let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options });

if (
// Requires SDK 50 for expo-assets hashAssetPlugin change.
!exp.sdkVersion ||
gteSdkVersion(exp, '50.0.0')
) {
// TODO: Handle asset prefix.
if (isExporting) {
// This token will be used in the asset plugin to ensure the path is correct for writing locally.
// @ts-expect-error: typed as readonly.
config.transformer.publicPath = '/assets?export_path=/assets';
} else {
// @ts-expect-error: typed as readonly
config.transformer.publicPath = '/assets/?unstable_path=.';
}
}

const platformBundlers = getPlatformBundlers(exp);

config = await withMetroMultiPlatformAsync(projectRoot, {
Expand All @@ -73,7 +109,8 @@ export async function loadMetroConfigAsync(
/** The most generic possible setup for Metro bundler. */
export async function instantiateMetroAsync(
metroBundler: MetroBundlerDevServer,
options: Omit<MetroDevServerOptions, 'logger'>
options: Omit<MetroDevServerOptions, 'logger'>,
{ isExporting }: { isExporting: boolean }
): Promise<{
metro: Metro.Server;
server: http.Server;
Expand All @@ -91,7 +128,7 @@ export async function instantiateMetroAsync(
const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync(
projectRoot,
options,
{ exp }
{ exp, isExporting }
);

const { middleware, websocketEndpoints, eventsSocketEndpoint, messageSocketEndpoint } =
Expand All @@ -118,6 +155,19 @@ export async function instantiateMetroAsync(
watch: isWatchEnabled(),
});

prependMiddleware(middleware, (req: ServerRequest, res: ServerResponse, next: ServerNext) => {
// If the URL is a Metro asset request, then we need to skip all other middleware to prevent
// the community CLI's serve-static from hosting `/assets/index.html` in place of all assets if it exists.
// /assets/?unstable_path=.
if (req.url) {
const url = new URL(req.url!, 'http://localhost:8000');
if (url.pathname.match(/^\/assets\/?/) && url.searchParams.get('unstable_path') != null) {
return metro.processRequest(req, res, next);
}
}
return next();
});

setEventReporter(eventsSocketEndpoint.reportEvent);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type ServerRequest = express.Request | http.IncomingMessage;
/** Response */
export type ServerResponse = express.Response | http.ServerResponse;
/** Next function */
export type ServerNext = (err?: Error) => void;
export type ServerNext = (err?: Error | null) => void;

/** The `connect()` app that is a http.RequestListener and having the `use()` function for middlewares. */
export interface ConnectAppType extends http.RequestListener {
Expand Down
2 changes: 2 additions & 0 deletions packages/expo-asset/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### 🛠 Breaking changes

- Convert `../` to `_` for the property `httpServerLocation` in `hashAssetFiles` (Metro asset pre-processor) to support assets in monorepos the same everywhere. ([#24090](https://github.com/expo/expo/pull/24090) by [@EvanBacon](https://github.com/EvanBacon))

### 🎉 New features

### 🐛 Bug fixes
Expand Down
12 changes: 11 additions & 1 deletion packages/expo-asset/tools/hashAssetFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
const md5File = require('md5-file/promise');

module.exports = function hashAssetFiles(asset) {
return Promise.all(asset.files.map(md5File)).then(hashes => {
return Promise.all(asset.files.map(md5File)).then((hashes) => {
asset.fileHashes = hashes;

// Convert the `../` segments of the server URL to `_` to support monorepos.
// This same transformation takes place in `AssetSourceResolver.web` (expo-assets, expo-image) and `persistMetroAssets` of Expo CLI,
// this originally came from the Metro opinion https://github.com/react-native-community/cli/blob/2204d357379e2067cebe2791e90388f7e97fc5f5/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts#L19C5-L19C10
if (asset.httpServerLocation.includes('?export_path=')) {
asset.httpServerLocation = asset.httpServerLocation
.match(/\?export_path=(.*)/)[1]
.replace(/\.\.\//g, '_');
}

return asset;
});
};
2 changes: 2 additions & 0 deletions packages/expo-dev-launcher/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 🐛 Bug fixes

- Use correct `httpServerLocation` files during export. ([#24090](https://github.com/expo/expo/pull/24090) by [@EvanBacon](https://github.com/EvanBacon))

### 💡 Others

- Migrate to Expo CLI. ([#23806](https://github.com/expo/expo/pull/23806) by [@EvanBacon](https://github.com/EvanBacon))
Expand Down
Loading

0 comments on commit 429dc7f

Please sign in to comment.