expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
272 lines • 11.7 kB
JavaScript
/**
* Copyright © 2024 650 Industries.
* Copyright © 2024 2023 Daishi Kato
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* https://github.com/dai-shi/waku/blob/32d52242c1450b5f5965860e671ff73c42da8bd0/packages/waku/src/client.ts#L1
*/
//// <reference types="react/canary" />
'use client';
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerRoot = exports.Children = exports.Slot = exports.useRefetch = exports.Root = exports.prefetchRSC = exports.fetchRSC = exports.callServerRSC = void 0;
const react_1 = require("react");
const client_1 = __importDefault(require("react-server-dom-webpack/client"));
const errors_1 = require("./errors");
const fetch_1 = require("./fetch");
const utils_1 = require("./utils");
const getDevServer_1 = require("../../getDevServer");
const url_1 = require("../../head/url");
const { createFromFetch, encodeReply } = client_1.default;
// TODO: Maybe this could be a bundler global instead.
const IS_DOM =
// @ts-expect-error: Added via react-native-webview
typeof ReactNativeWebView !== 'undefined';
// NOTE: Ensured to start with `/`.
const RSC_PATH = '/_flight/' + process.env.EXPO_OS; // process.env.EXPO_RSC_PATH;
// Using base URL for remote hosts isn't currently supported in DOM components as we use it for offline assets.
const BASE_URL = IS_DOM ? '' : process.env.EXPO_BASE_URL;
let BASE_PATH = `${BASE_URL}${RSC_PATH}`;
if (!BASE_PATH.startsWith('/')) {
BASE_PATH = '/' + BASE_PATH;
}
if (!BASE_PATH.endsWith('/')) {
BASE_PATH += '/';
}
if (BASE_PATH === '/') {
throw new Error(`Invalid React Flight path "${BASE_PATH}". The path should not live at the project root, e.g. /_flight/. Dev server URL: ${(0, getDevServer_1.getDevServer)().fullBundleUrl}`);
}
const RSC_CONTENT_TYPE = 'text/x-component';
const ENTRY = 'e';
const SET_ELEMENTS = 's';
const ON_FETCH_DATA = 'o';
const defaultFetchCache = {};
const NO_CACHE_HEADERS = process.env.EXPO_OS === 'web'
? {}
: // These are needed for iOS + Prod to get updates after the first request.
{
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
};
const ACTION_HEADERS = {
...NO_CACHE_HEADERS,
accept: RSC_CONTENT_TYPE,
'expo-platform': process.env.EXPO_OS,
};
const checkStatus = async (responsePromise) => {
// TODO: Combine with metro async fetch logic.
const response = await responsePromise;
if (!response.ok) {
// NOTE(EvanBacon): Transform the Metro development error into a JS error that can be used by LogBox.
// This was tested against using a Class component in a server component.
if (response.status === 500) {
const errorText = await response.text();
let errorJson;
try {
errorJson = JSON.parse(errorText);
}
catch {
throw new errors_1.ReactServerError(errorText, response.url, response.status);
}
// TODO: This should be a dev-only error. Add handling for production equivalent.
throw new errors_1.MetroServerError(errorJson, response.url);
}
let responseText;
try {
responseText = await response.text();
}
catch {
throw new errors_1.ReactServerError(response.statusText, response.url, response.status);
}
throw new errors_1.ReactServerError(responseText, response.url, response.status);
}
return response;
};
function getCached(c, m, k) {
return (m.has(k) ? m : m.set(k, c())).get(k);
}
const cache1 = new WeakMap();
const mergeElements = (a, b) => {
const getResult = () => {
const promise = new Promise((resolve, reject) => {
Promise.all([a, b])
.then(([a, b]) => {
const nextElements = { ...a, ...b };
delete nextElements._value;
promise.prev = a;
resolve(nextElements);
})
.catch((e) => {
a.then((a) => {
promise.prev = a;
reject(e);
}, () => {
promise.prev = a.prev;
reject(e);
});
});
});
return promise;
};
const cache2 = getCached(() => new WeakMap(), cache1, a);
return getCached(getResult, cache2, b);
};
/**
* callServer callback
* This is not a public API.
*/
const callServerRSC = async (actionId, args, fetchCache = defaultFetchCache) => {
const url = getAdjustedRemoteFilePath(BASE_PATH + (0, utils_1.encodeInput)((0, utils_1.encodeActionId)(actionId)));
const response = args === undefined
? (0, fetch_1.fetch)(url, { headers: ACTION_HEADERS })
: encodeReply(args).then((body) => (0, fetch_1.fetch)(url, { method: 'POST', body, headers: ACTION_HEADERS }));
const data = createFromFetch(checkStatus(response), {
callServer: (actionId, args) => (0, exports.callServerRSC)(actionId, args, fetchCache),
});
fetchCache[ON_FETCH_DATA]?.(data);
(0, react_1.startTransition)(() => {
// FIXME this causes rerenders even if data is empty
fetchCache[SET_ELEMENTS]?.((prev) => mergeElements(prev, data));
});
return (await data)._value;
};
exports.callServerRSC = callServerRSC;
const prefetchedParams = new WeakMap();
const fetchRSCInternal = (url, params) => params === undefined
? (0, fetch_1.fetch)(url, {
// Disable caching
headers: {
...NO_CACHE_HEADERS,
'expo-platform': process.env.EXPO_OS,
},
})
: typeof params === 'string'
? (0, fetch_1.fetch)(url, {
headers: {
...NO_CACHE_HEADERS,
'expo-platform': process.env.EXPO_OS,
'X-Expo-Params': params,
},
})
: encodeReply(params).then((body) => (0, fetch_1.fetch)(url, { method: 'POST', headers: ACTION_HEADERS, body }));
const fetchRSC = (input, params, fetchCache = defaultFetchCache) => {
// TODO: strip when "is exporting".
if (process.env.NODE_ENV === 'development') {
const refetchRsc = () => {
delete fetchCache[ENTRY];
const data = (0, exports.fetchRSC)(input, params, fetchCache);
fetchCache[SET_ELEMENTS]?.(() => data);
};
globalThis.__EXPO_RSC_RELOAD_LISTENERS__ ||= [];
const index = globalThis.__EXPO_RSC_RELOAD_LISTENERS__.indexOf(globalThis.__EXPO_REFETCH_RSC__);
if (index !== -1) {
globalThis.__EXPO_RSC_RELOAD_LISTENERS__.splice(index, 1, refetchRsc);
}
else {
globalThis.__EXPO_RSC_RELOAD_LISTENERS__.push(refetchRsc);
}
globalThis.__EXPO_REFETCH_RSC__ = refetchRsc;
}
const entry = fetchCache[ENTRY];
if (entry && entry[0] === input && entry[1] === params) {
return entry[2];
}
// eslint-disable-next-line no-multi-assign
const prefetched = (globalThis.__EXPO_PREFETCHED__ ||= {});
// TODO: Load from on-disk on native when indicated.
// const reqPath = fetchOptions?.remote ? getAdjustedRemoteFilePath(url) : getAdjustedRemoteFilePath(url);
const url = getAdjustedRemoteFilePath(BASE_PATH + (0, utils_1.encodeInput)(input));
const hasValidPrefetchedResponse = !!prefetched[url] &&
// HACK .has() is for the initial hydration
// It's limited and may result in a wrong result. FIXME
(!prefetchedParams.has(prefetched[url]) || prefetchedParams.get(prefetched[url]) === params);
const response = hasValidPrefetchedResponse ? prefetched[url] : fetchRSCInternal(url, params);
delete prefetched[url];
const data = createFromFetch(checkStatus(response), {
callServer: (actionId, args) => (0, exports.callServerRSC)(actionId, args, fetchCache),
});
fetchCache[ON_FETCH_DATA]?.(data);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
fetchCache[ENTRY] = [input, params, data];
return data;
};
exports.fetchRSC = fetchRSC;
function getAdjustedRemoteFilePath(path) {
if (IS_DOM && process.env.NODE_ENV === 'production') {
const origin = (0, url_1.getOriginFromConstants)();
if (!origin) {
throw new Error('Expo RSC: Origin not found in Constants. This is required for production DOM components using server actions.');
}
// DOM components in production need to use the same origin logic as native.
return new URL(path, origin).toString();
}
if (!IS_DOM && process.env.EXPO_OS === 'web') {
return path;
}
return new URL(path, window.location.href).toString();
}
const prefetchRSC = (input, params) => {
// eslint-disable-next-line no-multi-assign
const prefetched = (globalThis.__EXPO_PREFETCHED__ ||= {});
const url = getAdjustedRemoteFilePath(BASE_PATH + (0, utils_1.encodeInput)(input));
if (!(url in prefetched)) {
prefetched[url] = fetchRSCInternal(url, params);
prefetchedParams.set(prefetched[url], params);
}
};
exports.prefetchRSC = prefetchRSC;
const RefetchContext = (0, react_1.createContext)(() => {
throw new Error('Missing Root component');
});
const ElementsContext = (0, react_1.createContext)(null);
const Root = ({ initialInput, initialParams, fetchCache = defaultFetchCache, unstable_onFetchData, children, }) => {
fetchCache[ON_FETCH_DATA] = unstable_onFetchData;
const [elements, setElements] = (0, react_1.useState)(() => (0, exports.fetchRSC)(initialInput || '', initialParams, fetchCache));
(0, react_1.useEffect)(() => {
fetchCache[SET_ELEMENTS] = setElements;
}, [fetchCache, setElements]);
const refetch = (0, react_1.useCallback)((input, params) => {
// clear cache entry before fetching
delete fetchCache[ENTRY];
const data = (0, exports.fetchRSC)(input, params, fetchCache);
(0, react_1.startTransition)(() => {
setElements((prev) => mergeElements(prev, data));
});
}, [fetchCache]);
return (0, react_1.createElement)(RefetchContext.Provider, { value: refetch }, (0, react_1.createElement)(ElementsContext.Provider, { value: elements }, children));
};
exports.Root = Root;
const useRefetch = () => (0, react_1.use)(RefetchContext);
exports.useRefetch = useRefetch;
const ChildrenContext = (0, react_1.createContext)(undefined);
const ChildrenContextProvider = (0, react_1.memo)(ChildrenContext.Provider);
const Slot = ({ id, children, fallback, }) => {
const elementsPromise = (0, react_1.use)(ElementsContext);
if (!elementsPromise) {
throw new Error('Missing Root component');
}
const elements = (0, react_1.use)(elementsPromise);
if (!(id in elements)) {
if (fallback) {
return fallback;
}
throw new Error('Not found: ' + id + '. Expected: ' + Object.keys(elements).join(', '));
}
return (0, react_1.createElement)(ChildrenContextProvider, { value: children }, elements[id]);
};
exports.Slot = Slot;
const Children = () => (0, react_1.use)(ChildrenContext);
exports.Children = Children;
/**
* ServerRoot for SSR
* This is not a public API.
*/
const ServerRoot = ({ elements, children }) => (0, react_1.createElement)(ElementsContext.Provider, { value: elements }, children);
exports.ServerRoot = ServerRoot;
//# sourceMappingURL=host.js.map