UNPKG

expo-router

Version:

Expo Router is a file-based router for React Native and web applications.

366 lines (364 loc) • 14.8 kB
/** * 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'; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { Children: null, Root: null, ServerRoot: null, Slot: null, callServerRSC: null, fetchRSC: null, prefetchRSC: null, useRefetch: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: Object.getOwnPropertyDescriptor(all, name).get }); } _export(exports, { get Children () { return Children; }, get Root () { return Root; }, get ServerRoot () { return ServerRoot; }, get Slot () { return Slot; }, get callServerRSC () { return callServerRSC; }, get fetchRSC () { return fetchRSC; }, get prefetchRSC () { return prefetchRSC; }, get useRefetch () { return useRefetch; } }); function _expoconstants() { const data = /*#__PURE__*/ _interop_require_default(require("expo-constants")); _expoconstants = function() { return data; }; return data; } function _bundleorigin() { const data = require("expo/internal/bundle-origin"); _bundleorigin = function() { return data; }; return data; } function _react() { const data = require("react"); _react = function() { return data; }; return data; } function _client() { const data = /*#__PURE__*/ _interop_require_default(require("react-server-dom-webpack/client")); _client = function() { return data; }; return data; } const _url = require("../../head/url"); const _errors = require("./errors"); const _fetch = require("./fetch"); const _utils = require("./utils"); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { createFromFetch, encodeReply } = _client().default; // TODO: Maybe this could be a bundler global instead. const IS_DOM = // @ts-expect-error: Added via expo/dom typeof $$EXPO_INITIAL_PROPS !== '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, _bundleorigin().getBundleUrl)()}`); } if (process.env.EXPO_OS !== 'web' && !window.location?.href) { // This will require a rebuild in bare-workflow to update. const manifest = _expoconstants().default.expoConfig; const originFromConstants = manifest?.extra?.router?.origin ?? manifest?.extra?.router?.generatedOrigin; // In legacy cases, this can be extraneously set to false since it was the default before we had a production hosting solution for native servers. if (originFromConstants === false) { const isExpoGo = typeof expo !== 'undefined' && globalThis.expo?.modules?.ExpoGo; if (isExpoGo) { // Updating is a bit easier in Expo Go as you don't need a native rebuild. throw new Error('The "origin" property in the app config (app.json) cannot be false when React Server Components is enabled. https://docs.expo.dev/guides/server-components/'); } // Add more context about updating the app.json in development builds. throw new Error('The "origin" property in the app config (app.json) cannot be "false" when React Server Components is enabled. Remove the "origin" property from your Expo config and rebuild the native app to resolve. https://docs.expo.dev/guides/server-components/'); } // This can happen if the user attempts to use React Server Components without // enabling the flags in the app.json. This will set origin to false and prevent the expo/metro-runtime polyfill from running. throw new Error('window.location.href is not defined. This is required for React Server Components to work correctly. Ensure React Server Components is correctly enabled in your project and config. https://docs.expo.dev/guides/server-components/'); } 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' ? {} : { 'Cache-Control': 'no-cache', Pragma: 'no-cache', Expires: '0' }; // `expo-platform` is required server-side on action to force a cross-origin preflight check 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 (__DEV__ && (response.status === 500 || response.status === 404)) { const errorText = await response.text(); let errorJson; try { errorJson = JSON.parse(errorText); } catch { // `Unable to resolve module` error should respond as JSON from the dev server and sent to the master red box, this can get corrupt when it's returned as the formatted string. if (errorText.startsWith('Unable to resolve module')) { console.error('Unexpected Metro error format from dev server'); // This is an unexpected state that occurs when the dev server renderer does not throw Metro errors in the expected JSON format. throw new Error(errorJson); } throw new _errors.ReactServerError(errorText, response.url, response.status, response.headers); } throw new _errors.MetroServerError(errorJson, response.url); } let responseText; try { responseText = await response.text(); } catch { throw new _errors.ReactServerError(response.statusText, response.url, response.status, response.headers); } throw new _errors.ReactServerError(responseText, response.url, response.status, response.headers); } 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); }; const callServerRSC = async (actionId, args, fetchCache = defaultFetchCache)=>{ const url = getAdjustedRemoteFilePath(BASE_PATH + (0, _utils.encodeInput)((0, _utils.encodeActionId)(actionId))); const response = encodeReply(args ?? []).then((body)=>(0, _fetch.fetch)(url, { method: 'POST', body, headers: ACTION_HEADERS })); const data = createFromFetch(checkStatus(response), { callServer: (actionId, args)=>callServerRSC(actionId, args, fetchCache) }); fetchCache[ON_FETCH_DATA]?.(data); (0, _react().startTransition)(()=>{ // FIXME this causes rerenders even if data is empty fetchCache[SET_ELEMENTS]?.((prev)=>mergeElements(prev, data)); }); return (await data)._value; }; const prefetchedParams = new WeakMap(); const fetchRSCInternal = (url, params)=>params === undefined ? (0, _fetch.fetch)(url, { // Disable caching headers: { ...NO_CACHE_HEADERS, 'expo-platform': process.env.EXPO_OS } }) : typeof params === 'string' ? (0, _fetch.fetch)(url, { headers: { ...NO_CACHE_HEADERS, 'expo-platform': process.env.EXPO_OS, 'X-Expo-Params': params } }) : encodeReply(params).then((body)=>(0, _fetch.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 = 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.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)=>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; }; function getAdjustedRemoteFilePath(path) { if (IS_DOM && process.env.NODE_ENV === 'production') { const origin = (0, _url.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.encodeInput)(input)); if (!(url in prefetched)) { prefetched[url] = fetchRSCInternal(url, params); prefetchedParams.set(prefetched[url], params); } }; const RefetchContext = /*#__PURE__*/ (0, _react().createContext)(()=>{ throw new Error('Missing Root component'); }); const ElementsContext = /*#__PURE__*/ (0, _react().createContext)(null); const Root = ({ initialInput, initialParams, fetchCache = defaultFetchCache, unstable_onFetchData, children })=>{ fetchCache[ON_FETCH_DATA] = unstable_onFetchData; const [elements, setElements] = (0, _react().useState)(()=>fetchRSC(initialInput || '', initialParams, fetchCache)); (0, _react().useEffect)(()=>{ fetchCache[SET_ELEMENTS] = setElements; }, [ fetchCache, setElements ]); const refetch = (0, _react().useCallback)((input, params)=>{ // clear cache entry before fetching delete fetchCache[ENTRY]; const data = fetchRSC(input, params, fetchCache); (0, _react().startTransition)(()=>{ setElements((prev)=>mergeElements(prev, data)); }); }, [ fetchCache ]); return /*#__PURE__*/ (0, _react().createElement)(RefetchContext.Provider, { value: refetch }, /*#__PURE__*/ (0, _react().createElement)(ElementsContext.Provider, { value: elements }, children)); }; const useRefetch = ()=>(0, _react().use)(RefetchContext); const ChildrenContext = /*#__PURE__*/ (0, _react().createContext)(undefined); const ChildrenContextProvider = /*#__PURE__*/ (0, _react().memo)(ChildrenContext.Provider); const Slot = ({ id, children, fallback })=>{ const elementsPromise = (0, _react().use)(ElementsContext); if (!elementsPromise) { throw new Error('Missing Root component'); } const elements = (0, _react().use)(elementsPromise); if (!(id in elements)) { if (fallback) { return fallback; } throw new Error('Not found: ' + id + '. Expected: ' + Object.keys(elements).join(', ')); } return /*#__PURE__*/ (0, _react().createElement)(ChildrenContextProvider, { value: children }, elements[id]); }; const Children = ()=>(0, _react().use)(ChildrenContext); const ServerRoot = ({ elements, children })=>/*#__PURE__*/ (0, _react().createElement)(ElementsContext.Provider, { value: elements }, children); //# sourceMappingURL=host.js.map