diff --git a/examples/crm/package.json b/examples/crm/package.json index 68390d93184..7d24b6ce764 100644 --- a/examples/crm/package.json +++ b/examples/crm/package.json @@ -12,9 +12,10 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "faker": "~5.4.0", + "fakerest": "4.1.3", "lodash": "~4.17.5", "papaparse": "^5.4.1", - "ra-data-fakerest": "^5.3.0", + "ra-data-simple-rest": "^5.3.0", "react": "^18.3.1", "react-admin": "^5.3.0", "react-cropper": "^2.3.3", @@ -46,5 +47,10 @@ "build": "tsc && vite build", "type-check": "tsc", "preview": "vite preview" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/examples/crm/public/mockServiceWorker.js b/examples/crm/public/mockServiceWorker.js new file mode 100644 index 00000000000..be4527c7ee5 --- /dev/null +++ b/examples/crm/public/mockServiceWorker.js @@ -0,0 +1,344 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.10.4' +const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/examples/crm/src/index.tsx b/examples/crm/src/index.tsx index bbda3918465..076c49c7740 100644 --- a/examples/crm/src/index.tsx +++ b/examples/crm/src/index.tsx @@ -2,15 +2,18 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { worker } from './providers/fakerest/fakeServer'; const container = document.getElementById('root'); const root = createRoot(container!); -root.render( - - - -); +worker.start({ onUnhandledRequest: 'bypass', quiet: true }).then(() => { + root.render( + + + + ); +}); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) diff --git a/examples/crm/src/providers/fakerest/dataProvider.ts b/examples/crm/src/providers/fakerest/dataProvider.ts index 867886d0066..b88b445741e 100644 --- a/examples/crm/src/providers/fakerest/dataProvider.ts +++ b/examples/crm/src/providers/fakerest/dataProvider.ts @@ -1,4 +1,4 @@ -import fakeRestDataProvider from 'ra-data-fakerest'; +import simpleRestProvider from 'ra-data-simple-rest'; import { CreateParams, DataProvider, @@ -20,10 +20,9 @@ import { getActivityLog } from '../commons/activity'; import { getCompanyAvatar } from '../commons/getCompanyAvatar'; import { getContactAvatar } from '../commons/getContactAvatar'; import { authProvider, USER_STORAGE_KEY } from './authProvider'; -import generateData from './dataGenerator'; import { withSupabaseFilterAdapter } from './internal/supabaseAdapter'; -const baseDataProvider = fakeRestDataProvider(generateData(), true, 300); +const baseDataProvider = simpleRestProvider('http://localhost:4000'); const TASK_MARKED_AS_DONE = 'TASK_MARKED_AS_DONE'; const TASK_MARKED_AS_UNDONE = 'TASK_MARKED_AS_UNDONE'; diff --git a/examples/crm/src/providers/fakerest/fakeServer.ts b/examples/crm/src/providers/fakerest/fakeServer.ts new file mode 100644 index 00000000000..7e207281546 --- /dev/null +++ b/examples/crm/src/providers/fakerest/fakeServer.ts @@ -0,0 +1,13 @@ +import { http } from 'msw'; +import { setupWorker } from 'msw/browser'; +import { type CollectionItem, getMswHandler, withDelay } from 'fakerest'; +import generateData from './dataGenerator'; + +const handler = getMswHandler({ + baseUrl: 'http://localhost:4000', + data: generateData() as CollectionItem, + middlewares: [withDelay(300)], +}); +export const worker = setupWorker(http.all(/http:\/\/localhost:4000/, handler)); + +export default () => worker; diff --git a/examples/demo/package.json b/examples/demo/package.json index 3fbd40e0637..136c62168af 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -14,7 +14,6 @@ "date-fns": "^3.6.0", "echarts": "^5.6.0", "fakerest": "^4.1.1", - "fetch-mock": "^9.11.0", "graphql": "^15.6.0", "graphql-tag": "^2.12.6", "inflection": "^3.0.0", @@ -40,15 +39,20 @@ "preview": "vite preview" }, "devDependencies": { - "@types/fetch-mock": "^7.3.2", "@types/jest": "^29.5.2", "@types/node": "^20.10.7", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.4", + "msw": "^2.10.4", "rollup-plugin-visualizer": "^5.14.0", "rollup-preserve-directives": "^1.1.3", "typescript": "^5.1.3", "vite": "^6.2.6" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/examples/demo/public/mockServiceWorker.js b/examples/demo/public/mockServiceWorker.js new file mode 100644 index 00000000000..be4527c7ee5 --- /dev/null +++ b/examples/demo/public/mockServiceWorker.js @@ -0,0 +1,344 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.10.4' +const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/examples/demo/src/dataProvider/index.ts b/examples/demo/src/dataProvider/index.ts index 9beebb43cf5..66a7ee090c5 100644 --- a/examples/demo/src/dataProvider/index.ts +++ b/examples/demo/src/dataProvider/index.ts @@ -1,5 +1,4 @@ import { DataProvider } from 'react-admin'; -import fakeServerFactory from '../fakeServer'; export default (type: string) => { // The fake servers require to generate data, which can take some time. @@ -29,7 +28,6 @@ export default (type: string) => { }; const getDataProvider = async (type: string): Promise => { - await fakeServerFactory(process.env.REACT_APP_DATA_PROVIDER || ''); /** * This demo can work with either a fake REST server, or a fake GraphQL server. * diff --git a/examples/demo/src/fakeServer/graphql.ts b/examples/demo/src/fakeServer/graphql.ts index 2f8d133140e..ec0f56be434 100644 --- a/examples/demo/src/fakeServer/graphql.ts +++ b/examples/demo/src/fakeServer/graphql.ts @@ -1,29 +1,21 @@ import JsonGraphqlServer from 'json-graphql-server'; import generateData from 'data-generator-retail'; -import fetchMock from 'fetch-mock'; +import { HttpResponse, HttpResponseResolver } from 'msw'; -export default () => { +export default (): HttpResponseResolver => { const data = generateData(); - const restServer = JsonGraphqlServer({ data }); - const handler = restServer.getHandler(); - const handlerWithLogs = (url: string, opts: any) => - handler(url, opts).then((res: any) => { - const req = JSON.parse(opts.body); - const parsedRes = JSON.parse(res.body); - console.groupCollapsed(`GraphQL ${req.operationName}`); - console.group('request'); - console.log('operationName', req.operationName); - console.log(req.query); - console.log('variables', req.variables); - console.groupEnd(); - console.group('response'); - console.log('data', parsedRes.data); - console.log('errors', parsedRes.errors); - console.groupEnd(); - console.groupEnd(); - return res; + const server = JsonGraphqlServer({ data }); + const graphqlHandler = server.getHandler(); + + // Temporary workaround for MSW's graphql handler because json-graphql-server is not yet compatible with MSW + const handler: HttpResponseResolver = async ({ request }) => { + const body = await request.text(); + const result = await graphqlHandler({ + requestBody: body, }); - fetchMock.mock('begin:http://localhost:4000', handlerWithLogs); - return () => fetchMock.restore(); + return HttpResponse.json(JSON.parse(result.body)); + }; + + return handler; }; diff --git a/examples/demo/src/fakeServer/index.ts b/examples/demo/src/fakeServer/index.ts index b39881fab97..e97f3c2d57c 100644 --- a/examples/demo/src/fakeServer/index.ts +++ b/examples/demo/src/fakeServer/index.ts @@ -1,20 +1,23 @@ // only install the mocks once // this is necessary with react@18 in StrictMode let fakeServer: any; +import { http } from 'msw'; +import { setupWorker } from 'msw/browser'; -export default (type: string) => { +export default async (type: string) => { if (!fakeServer) { switch (type) { case 'graphql': - fakeServer = import('./graphql').then(factory => + fakeServer = await import('./graphql').then(factory => factory.default() ); break; default: - fakeServer = import('./rest').then(factory => + fakeServer = await import('./rest').then(factory => factory.default() ); } } - return fakeServer; + const worker = setupWorker(http.all(/http:\/\/localhost:4000/, fakeServer)); + return worker; }; diff --git a/examples/demo/src/fakeServer/rest.ts b/examples/demo/src/fakeServer/rest.ts index b81a35e6e69..3111fa4e217 100644 --- a/examples/demo/src/fakeServer/rest.ts +++ b/examples/demo/src/fakeServer/rest.ts @@ -1,18 +1,16 @@ -import { FetchMockAdapter, withDelay } from 'fakerest'; -import fetchMock from 'fetch-mock'; +import { MswAdapter, withDelay } from 'fakerest'; import generateData from 'data-generator-retail'; export default () => { const data = generateData(); - const adapter = new FetchMockAdapter({ + const adapter = new MswAdapter({ baseUrl: 'http://localhost:4000', data, loggingEnabled: true, - middlewares: [withDelay(500)], + middlewares: [withDelay(300)], }); if (window) { window.restServer = adapter.server; // give way to update data in the console } - fetchMock.mock('begin:http://localhost:4000', adapter.getHandler()); - return () => fetchMock.restore(); + return adapter.getHandler(); }; diff --git a/examples/demo/src/index.tsx b/examples/demo/src/index.tsx index 6743a50f12e..82c2a47a203 100644 --- a/examples/demo/src/index.tsx +++ b/examples/demo/src/index.tsx @@ -1,6 +1,7 @@ import { createRoot } from 'react-dom/client'; import App from './App'; +import fakeServerWorker from './fakeServer'; const container = document.getElementById('root'); if (!container) { @@ -8,4 +9,14 @@ if (!container) { } const root = createRoot(container); -root.render(); +fakeServerWorker(process.env.REACT_APP_DATA_PROVIDER ?? '') + .then(worker => + worker.start({ + onUnhandledRequest: 'bypass', + quiet: true, + serviceWorker: { url: './mockServiceWorker.js' }, + }) + ) + .then(() => { + root.render(); + }); diff --git a/examples/simple/package.json b/examples/simple/package.json index 4cd255ef1b3..a8889da84bf 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -14,9 +14,10 @@ "@mui/material": "^5.16.12", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0", + "fakerest": "^4.1.3", "jsonexport": "^3.2.0", "lodash": "~4.17.5", - "ra-data-fakerest": "^5.10.0", + "ra-data-simple-rest": "^5.10.0", "ra-i18n-polyglot": "^5.10.0", "ra-input-rich-text": "^5.10.0", "ra-language-english": "^5.10.0", @@ -35,5 +36,10 @@ "react-simple-animate": "^3.5.3", "typescript": "^5.1.3", "vite": "^6.2.6" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/examples/simple/public/mockServiceWorker.js b/examples/simple/public/mockServiceWorker.js new file mode 100644 index 00000000000..be4527c7ee5 --- /dev/null +++ b/examples/simple/public/mockServiceWorker.js @@ -0,0 +1,344 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.10.4' +const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + */ +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @returns {Promise} + */ +async function getResponse(event, client, requestId) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/examples/simple/src/dataProvider.tsx b/examples/simple/src/dataProvider.tsx index ce097c92e55..ba0adc8e8a3 100644 --- a/examples/simple/src/dataProvider.tsx +++ b/examples/simple/src/dataProvider.tsx @@ -1,11 +1,12 @@ -import fakeRestProvider from 'ra-data-fakerest'; +import simpleRestProvider from 'ra-data-simple-rest'; import { DataProvider, withLifecycleCallbacks, HttpError } from 'react-admin'; import get from 'lodash/get'; -import data from './data'; import addUploadFeature from './addUploadFeature'; import { queryClient } from './queryClient'; -const dataProvider = withLifecycleCallbacks(fakeRestProvider(data, true, 300), [ +const defaultDataProvider = simpleRestProvider('http://localhost:4000'); + +const dataProvider = withLifecycleCallbacks(defaultDataProvider, [ { resource: 'posts', beforeDelete: async ({ id }, dp) => { diff --git a/examples/simple/src/fakeServer.ts b/examples/simple/src/fakeServer.ts new file mode 100644 index 00000000000..14bfa38dc03 --- /dev/null +++ b/examples/simple/src/fakeServer.ts @@ -0,0 +1,13 @@ +import data from './data'; +import { http } from 'msw'; +import { setupWorker } from 'msw/browser'; +import { getMswHandler, withDelay } from 'fakerest'; + +const handler = getMswHandler({ + baseUrl: 'http://localhost:4000', + data, + middlewares: [withDelay(300)], +}); +export const worker = setupWorker(http.all(/http:\/\/localhost:4000/, handler)); + +export default () => worker; diff --git a/examples/simple/src/index.tsx b/examples/simple/src/index.tsx index e83d410c954..723832124bf 100644 --- a/examples/simple/src/index.tsx +++ b/examples/simple/src/index.tsx @@ -15,48 +15,57 @@ import posts from './posts'; import users from './users'; import tags from './tags'; import { queryClient } from './queryClient'; +import { worker } from './fakeServer'; const container = document.getElementById('root') as HTMLElement; const root = createRoot(container); -root.render( - - - - - - - - } - /> - - } - /> - - - } - /> - - - } - /> - - - -); +worker.start({ onUnhandledRequest: 'bypass', quiet: true }).then(() => { + root.render( + + + + + + + + + } + /> + + } + /> + + + + } + /> + + + + } + /> + + + + ); +}); diff --git a/yarn.lock b/yarn.lock index 2b574783337..90b65f035cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,7 +87,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.0.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.26.0": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.23.0, @babel/core@npm:^7.26.0": version: 7.26.10 resolution: "@babel/core@npm:7.26.10" dependencies: @@ -1480,7 +1480,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.21.5, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.21.5, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.27.0 resolution: "@babel/runtime@npm:7.27.0" dependencies: @@ -1532,6 +1532,34 @@ __metadata: languageName: node linkType: hard +"@bundled-es-modules/cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "@bundled-es-modules/cookie@npm:2.0.1" + dependencies: + cookie: "npm:^0.7.2" + checksum: dfac5e36127e827c5557b8577f17a8aa94c057baff6d38555917927b99da0ecf0b1357e7fedadc8853ecdbd4a8a7fa1f5e64111b2a656612f4a36376f5bdbe8d + languageName: node + linkType: hard + +"@bundled-es-modules/statuses@npm:^1.0.1": + version: 1.0.1 + resolution: "@bundled-es-modules/statuses@npm:1.0.1" + dependencies: + statuses: "npm:^2.0.1" + checksum: c1a8ede3efa8da61ccda4b98e773582a9733edfbeeee569d4630785f8e018766202edb190a754a3ec7a7f6bd738e857829affc2fdb676b6dab4db1bb44e62785 + languageName: node + linkType: hard + +"@bundled-es-modules/tough-cookie@npm:^0.1.6": + version: 0.1.6 + resolution: "@bundled-es-modules/tough-cookie@npm:0.1.6" + dependencies: + "@types/tough-cookie": "npm:^4.0.5" + tough-cookie: "npm:^4.1.4" + checksum: 28bcac878bff6b34719ba3aa8341e9924772ee55de5487680ebe784981ec9fccb70ed5d46f563e2404855a04de606f9e56aa4202842d4f5835bc04a4fe820571 + languageName: node + linkType: hard + "@cypress/request@npm:^2.88.10": version: 2.88.10 resolution: "@cypress/request@npm:2.88.10" @@ -2150,6 +2178,61 @@ __metadata: languageName: node linkType: hard +"@inquirer/confirm@npm:^5.0.0": + version: 5.1.14 + resolution: "@inquirer/confirm@npm:5.1.14" + dependencies: + "@inquirer/core": "npm:^10.1.15" + "@inquirer/type": "npm:^3.0.8" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 12f49e8d1564c77c290163e87c9a256cfc087eab0c096738c73b03aa3d59a98c233fb9fb3692f162d67f923d120a4aa8ef819f75d979916dc13456f726c579d1 + languageName: node + linkType: hard + +"@inquirer/core@npm:^10.1.15": + version: 10.1.15 + resolution: "@inquirer/core@npm:10.1.15" + dependencies: + "@inquirer/figures": "npm:^1.0.13" + "@inquirer/type": "npm:^3.0.8" + ansi-escapes: "npm:^4.3.2" + cli-width: "npm:^4.1.0" + mute-stream: "npm:^2.0.0" + signal-exit: "npm:^4.1.0" + wrap-ansi: "npm:^6.2.0" + yoctocolors-cjs: "npm:^2.1.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 3214dfa882f17e3d9cdd45fc73f9134b90e3d685f8285f7963d836fe25f786d8ecf9c16d2710fc968b77da40508fa74466d5ad90c5466626037995210b946b12 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.13": + version: 1.0.13 + resolution: "@inquirer/figures@npm:1.0.13" + checksum: 23700a4a0627963af5f51ef4108c338ae77bdd90393164b3fdc79a378586e1f5531259882b7084c690167bf5a36e83033e45aca0321570ba810890abe111014f + languageName: node + linkType: hard + +"@inquirer/type@npm:^3.0.8": + version: 3.0.8 + resolution: "@inquirer/type@npm:3.0.8" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 1171bffb9ea0018b12ec4f46a7b485f7e2a328e620e89f3b03f2be8c25889e5b9e62daca3ea10ed040a71d847066c4d9879dc1fea8aa5690ebbc968d3254a5ac + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -2569,6 +2652,20 @@ __metadata: languageName: node linkType: hard +"@mswjs/interceptors@npm:^0.39.1": + version: 0.39.3 + resolution: "@mswjs/interceptors@npm:0.39.3" + dependencies: + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/logger": "npm:^0.3.0" + "@open-draft/until": "npm:^2.0.0" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + strict-event-emitter: "npm:^0.5.1" + checksum: 1caa88a6bfee22d717472a44327b42dfeef939b237905239100263fedd10bd16d514dc5c26407fc6941fcd160027d47201b106a1ed26de53880aa2d676e0665f + languageName: node + linkType: hard + "@mui/core-downloads-tracker@npm:^5.16.14": version: 5.16.14 resolution: "@mui/core-downloads-tracker@npm:5.16.14" @@ -3668,6 +3765,30 @@ __metadata: languageName: node linkType: hard +"@open-draft/deferred-promise@npm:^2.2.0": + version: 2.2.0 + resolution: "@open-draft/deferred-promise@npm:2.2.0" + checksum: eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549 + languageName: node + linkType: hard + +"@open-draft/logger@npm:^0.3.0": + version: 0.3.0 + resolution: "@open-draft/logger@npm:0.3.0" + dependencies: + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.0" + checksum: 90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1 + languageName: node + linkType: hard + +"@open-draft/until@npm:^2.0.0, @open-draft/until@npm:^2.1.0": + version: 2.1.0 + resolution: "@open-draft/until@npm:2.1.0" + checksum: 61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf + languageName: node + linkType: hard + "@parcel/watcher@npm:2.0.4": version: 2.0.4 resolution: "@parcel/watcher@npm:2.0.4" @@ -4889,6 +5010,13 @@ __metadata: languageName: node linkType: hard +"@types/cookie@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/cookie@npm:0.6.0" + checksum: 5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149 + languageName: node + linkType: hard + "@types/d3-path@npm:^1": version: 1.0.9 resolution: "@types/d3-path@npm:1.0.9" @@ -4954,13 +5082,6 @@ __metadata: languageName: node linkType: hard -"@types/fetch-mock@npm:^7.3.2": - version: 7.3.5 - resolution: "@types/fetch-mock@npm:7.3.5" - checksum: c13807feb486abd496b81db96ea098dfc13de09961ba02c42f44e89ec23ddd32e0be5c60c12a512718578d53360c12051001f162cae1acde6b13781095f29431 - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.6 resolution: "@types/graceful-fs@npm:4.1.6" @@ -5243,6 +5364,13 @@ __metadata: languageName: node linkType: hard +"@types/statuses@npm:^2.0.4": + version: 2.0.6 + resolution: "@types/statuses@npm:2.0.6" + checksum: dd88c220b0e2c6315686289525fd61472d2204d2e4bef4941acfb76bda01d3066f749ac74782aab5b537a45314fcd7d6261eefa40b6ec872691f5803adaa608d + languageName: node + linkType: hard + "@types/throttle-debounce@npm:^2.1.0": version: 2.1.0 resolution: "@types/throttle-debounce@npm:2.1.0" @@ -5257,6 +5385,13 @@ __metadata: languageName: node linkType: hard +"@types/tough-cookie@npm:^4.0.5": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: 68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473 + languageName: node + linkType: hard + "@types/trusted-types@npm:*, @types/trusted-types@npm:^2.0.7": version: 2.0.7 resolution: "@types/trusted-types@npm:2.0.7" @@ -6079,7 +6214,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -7361,6 +7496,13 @@ __metadata: languageName: node linkType: hard +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -7734,6 +7876,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + "cookie@npm:^1.0.1": version: 1.0.2 resolution: "cookie@npm:1.0.2" @@ -7750,13 +7899,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.0.0": - version: 3.20.3 - resolution: "core-js@npm:3.20.3" - checksum: 7c53c1971572cc6bcfb89eef612d1602fdd97cc8872172ea64a73f5216c3ef43af472a40be76994cbf854b3fb11dd7e01bbbbad0177e0ec79e502c1d86b516fe - languageName: node - linkType: hard - "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -8432,7 +8574,6 @@ __metadata: "@apollo/client": "npm:^3.12.4" "@mui/icons-material": "npm:^7.0.0" "@mui/material": "npm:^7.0.0" - "@types/fetch-mock": "npm:^7.3.2" "@types/jest": "npm:^29.5.2" "@types/node": "npm:^20.10.7" "@types/react": "npm:^18.3.3" @@ -8444,11 +8585,11 @@ __metadata: date-fns: "npm:^3.6.0" echarts: "npm:^5.6.0" fakerest: "npm:^4.1.1" - fetch-mock: "npm:^9.11.0" graphql: "npm:^15.6.0" graphql-tag: "npm:^2.12.6" inflection: "npm:^3.0.0" json-graphql-server: "npm:^3.0.1" + msw: "npm:^2.10.4" query-string: "npm:^7.1.3" ra-data-graphql: "npm:^5.0.0" ra-data-graphql-simple: "npm:^5.0.0" @@ -9862,12 +10003,12 @@ __metadata: languageName: node linkType: hard -"fakerest@npm:^4.0.1, fakerest@npm:^4.1.1": - version: 4.1.1 - resolution: "fakerest@npm:4.1.1" +"fakerest@npm:4.1.3, fakerest@npm:^4.0.1, fakerest@npm:^4.1.1, fakerest@npm:^4.1.3": + version: 4.1.3 + resolution: "fakerest@npm:4.1.3" dependencies: lodash: "npm:^4.17.21" - checksum: 69214b69e23d4ad236c4ff037fe8a45611b0db71f171a40c427942438b2b4be736aea0bded2c16b44a410218386993cc8dc91782e801518e4193917819c28517 + checksum: 535064d032a6bec88aa9b3e33d6f84e0622bee1f722a821abb8d6e6ca9e65102e11dd800c81be8c6a3fb97bb80875142e809b412e4a434c4fb9dd1911367b6dc languageName: node linkType: hard @@ -9965,29 +10106,6 @@ __metadata: languageName: node linkType: hard -"fetch-mock@npm:^9.11.0": - version: 9.11.0 - resolution: "fetch-mock@npm:9.11.0" - dependencies: - "@babel/core": "npm:^7.0.0" - "@babel/runtime": "npm:^7.0.0" - core-js: "npm:^3.0.0" - debug: "npm:^4.1.1" - glob-to-regexp: "npm:^0.4.0" - is-subset: "npm:^0.1.1" - lodash.isequal: "npm:^4.5.0" - path-to-regexp: "npm:^2.2.1" - querystring: "npm:^0.2.0" - whatwg-url: "npm:^6.5.0" - peerDependencies: - node-fetch: "*" - peerDependenciesMeta: - node-fetch: - optional: true - checksum: 1bc2a83b34c10ad412ee381b5f9ee64c4daa6390f9b42a76ae7dc9dcbb5a6795f7f8748e315fcafe612e7a8abd05c8f2d3554a36d576f22e12497010d9dd8e4c - languageName: node - linkType: hard - "figures@npm:3.2.0, figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -10723,7 +10841,7 @@ __metadata: languageName: node linkType: hard -"glob-to-regexp@npm:^0.4.0, glob-to-regexp@npm:^0.4.1": +"glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" checksum: 0486925072d7a916f052842772b61c3e86247f0a80cc0deb9b5a3e8a1a9faad5b04fb6f58986a09f34d3e96cd2a22a24b7e9882fb1cf904c31e9a310de96c429 @@ -11058,6 +11176,13 @@ __metadata: languageName: node linkType: hard +"headers-polyfill@npm:^4.0.2": + version: 4.0.3 + resolution: "headers-polyfill@npm:4.0.3" + checksum: 53e85b2c6385f8d411945fb890c5369f1469ce8aa32a6e8d28196df38568148de640c81cf88cbc7c67767103dd9acba48f4f891982da63178fc6e34560022afe + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -11898,6 +12023,13 @@ __metadata: languageName: node linkType: hard +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed + languageName: node + linkType: hard + "is-number-object@npm:^1.1.1": version: 1.1.1 resolution: "is-number-object@npm:1.1.1" @@ -12034,13 +12166,6 @@ __metadata: languageName: node linkType: hard -"is-subset@npm:^0.1.1": - version: 0.1.1 - resolution: "is-subset@npm:0.1.1" - checksum: d8125598ab9077a76684e18726fb915f5cea7a7358ed0c6ff723f4484d71a0a9981ee5aae06c44de99cfdef0fefce37438c6257ab129e53c82045ea0c2acdebf - languageName: node - linkType: hard - "is-symbol@npm:^1.0.4, is-symbol@npm:^1.1.1": version: 1.1.1 resolution: "is-symbol@npm:1.1.1" @@ -13524,13 +13649,6 @@ __metadata: languageName: node linkType: hard -"lodash.sortby@npm:^4.7.0": - version: 4.7.0 - resolution: "lodash.sortby@npm:4.7.0" - checksum: fc48fb54ff7669f33bb32997cab9460757ee99fafaf72400b261c3e10fde21538e47d8cfcbe6a25a31bcb5b7b727c27d52626386fc2de24eb059a6d64a89cdf5 - languageName: node - linkType: hard - "lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.5": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -14190,6 +14308,39 @@ __metadata: languageName: node linkType: hard +"msw@npm:^2.10.4": + version: 2.10.4 + resolution: "msw@npm:2.10.4" + dependencies: + "@bundled-es-modules/cookie": "npm:^2.0.1" + "@bundled-es-modules/statuses": "npm:^1.0.1" + "@bundled-es-modules/tough-cookie": "npm:^0.1.6" + "@inquirer/confirm": "npm:^5.0.0" + "@mswjs/interceptors": "npm:^0.39.1" + "@open-draft/deferred-promise": "npm:^2.2.0" + "@open-draft/until": "npm:^2.1.0" + "@types/cookie": "npm:^0.6.0" + "@types/statuses": "npm:^2.0.4" + graphql: "npm:^16.8.1" + headers-polyfill: "npm:^4.0.2" + is-node-process: "npm:^1.2.0" + outvariant: "npm:^1.4.3" + path-to-regexp: "npm:^6.3.0" + picocolors: "npm:^1.1.1" + strict-event-emitter: "npm:^0.5.1" + type-fest: "npm:^4.26.1" + yargs: "npm:^17.7.2" + peerDependencies: + typescript: ">= 4.8.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 48dff36c7cf8ad504bb8f8a2ff6946cf5727752c140681eb68da00991d9fe56224bace970476771a9fffae136256c389c591d71368a6967d053dbad6b6df3346 + languageName: node + linkType: hard + "multimatch@npm:5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" @@ -14210,6 +14361,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 2cf48a2087175c60c8dcdbc619908b49c07f7adcfc37d29236b0c5c612d6204f789104c98cc44d38acab7b3c96f4a3ec2cfdc4934d0738d876dbefa2a12c69f4 + languageName: node + linkType: hard + "mute-stream@npm:~1.0.0": version: 1.0.0 resolution: "mute-stream@npm:1.0.0" @@ -14951,6 +15109,13 @@ __metadata: languageName: node linkType: hard +"outvariant@npm:^1.4.0, outvariant@npm:^1.4.3": + version: 1.4.3 + resolution: "outvariant@npm:1.4.3" + checksum: 5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c + languageName: node + linkType: hard + "own-keys@npm:^1.0.1": version: 1.0.1 resolution: "own-keys@npm:1.0.1" @@ -15329,10 +15494,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^2.2.1": - version: 2.4.0 - resolution: "path-to-regexp@npm:2.4.0" - checksum: 286e3f2ea633ae9447d9c6beb2974db67801ffd64927e70129604e1fb987d94c9b38fa70cc494cd088c78d521914edec87de7f11928848c9ac265632c8644fc8 +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: 73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 languageName: node linkType: hard @@ -16013,13 +16178,6 @@ __metadata: languageName: node linkType: hard -"querystring@npm:^0.2.0": - version: 0.2.1 - resolution: "querystring@npm:0.2.1" - checksum: 6841b32bec4f16ffe7f5b5e4373b47ad451f079cde3a7f45e63e550f0ecfd8f8189ef81fb50079413b3fc1c59b06146e4c98192cb74ed7981aca72090466cd94 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -16085,7 +16243,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-fakerest@npm:^5.10.0, ra-data-fakerest@npm:^5.3.0, ra-data-fakerest@workspace:packages/ra-data-fakerest": +"ra-data-fakerest@npm:^5.10.0, ra-data-fakerest@workspace:packages/ra-data-fakerest": version: 0.0.0-use.local resolution: "ra-data-fakerest@workspace:packages/ra-data-fakerest" dependencies: @@ -16179,7 +16337,7 @@ __metadata: languageName: unknown linkType: soft -"ra-data-simple-rest@npm:^5.0.0, ra-data-simple-rest@workspace:packages/ra-data-simple-rest": +"ra-data-simple-rest@npm:^5.0.0, ra-data-simple-rest@npm:^5.10.0, ra-data-simple-rest@npm:^5.3.0, ra-data-simple-rest@workspace:packages/ra-data-simple-rest": version: 0.0.0-use.local resolution: "ra-data-simple-rest@workspace:packages/ra-data-simple-rest" dependencies: @@ -16443,9 +16601,10 @@ __metadata: clsx: "npm:^2.1.1" date-fns: "npm:^3.6.0" faker: "npm:~5.4.0" + fakerest: "npm:4.1.3" lodash: "npm:~4.17.5" papaparse: "npm:^5.4.1" - ra-data-fakerest: "npm:^5.3.0" + ra-data-simple-rest: "npm:^5.3.0" react: "npm:^18.3.1" react-admin: "npm:^5.3.0" react-cropper: "npm:^2.3.3" @@ -17853,10 +18012,10 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.0.2 - resolution: "signal-exit@npm:4.0.2" - checksum: 3c36ae214f4774b4a7cbbd2d090b2864f8da4dc3f9140ba5b76f38bea7605c7aa8042adf86e48ee8a0955108421873f9b0f20281c61b8a65da4d9c1c1de4929f +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard @@ -17884,10 +18043,11 @@ __metadata: "@tanstack/react-query": "npm:^5.83.0" "@tanstack/react-query-devtools": "npm:^5.83.0" "@vitejs/plugin-react": "npm:^4.3.4" + fakerest: "npm:^4.1.3" jsonexport: "npm:^3.2.0" little-state-machine: "npm:^4.8.1" lodash: "npm:~4.17.5" - ra-data-fakerest: "npm:^5.10.0" + ra-data-simple-rest: "npm:^5.10.0" ra-i18n-polyglot: "npm:^5.10.0" ra-input-rich-text: "npm:^5.10.0" ra-language-english: "npm:^5.10.0" @@ -18207,6 +18367,13 @@ __metadata: languageName: node linkType: hard +"statuses@npm:^2.0.1": + version: 2.0.2 + resolution: "statuses@npm:2.0.2" + checksum: a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f + languageName: node + linkType: hard + "storybook@npm:^8.6.11": version: 8.6.11 resolution: "storybook@npm:8.6.11" @@ -18225,6 +18392,13 @@ __metadata: languageName: node linkType: hard +"strict-event-emitter@npm:^0.5.1": + version: 0.5.1 + resolution: "strict-event-emitter@npm:0.5.1" + checksum: f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -18790,15 +18964,15 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.1.2": - version: 4.1.3 - resolution: "tough-cookie@npm:4.1.3" +"tough-cookie@npm:^4.1.2, tough-cookie@npm:^4.1.4": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" dependencies: psl: "npm:^1.1.33" punycode: "npm:^2.1.1" universalify: "npm:^0.2.0" url-parse: "npm:^1.5.3" - checksum: 4fc0433a0cba370d57c4b240f30440c848906dee3180bb6e85033143c2726d322e7e4614abb51d42d111ebec119c4876ed8d7247d4113563033eebbc1739c831 + checksum: aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45 languageName: node linkType: hard @@ -18812,15 +18986,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^1.0.1": - version: 1.0.1 - resolution: "tr46@npm:1.0.1" - dependencies: - punycode: "npm:^2.1.0" - checksum: 41525c2ccce86e3ef30af6fa5e1464e6d8bb4286a58ea8db09228f598889581ef62347153f6636cd41553dc41685bdfad0a9d032ef58df9fbb0792b3447d0f04 - languageName: node - linkType: hard - "tr46@npm:^3.0.0": version: 3.0.0 resolution: "tr46@npm:3.0.0" @@ -19072,6 +19237,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.26.1": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 + languageName: node + linkType: hard + "type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -19709,13 +19881,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^4.0.2": - version: 4.0.2 - resolution: "webidl-conversions@npm:4.0.2" - checksum: def5c5ac3479286dffcb604547628b2e6b46c5c5b8a8cfaa8c71dc3bafc85859bde5fbe89467ff861f571ab38987cf6ab3d6e7c80b39b999e50e803c12f3164f - languageName: node - linkType: hard - "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0" @@ -19845,17 +20010,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^6.5.0": - version: 6.5.0 - resolution: "whatwg-url@npm:6.5.0" - dependencies: - lodash.sortby: "npm:^4.7.0" - tr46: "npm:^1.0.1" - webidl-conversions: "npm:^4.0.2" - checksum: 5afeff7da025fbaecceca6a5e0cdc6d10666efab245d0e5d785263a09a16b3afce7a81712512e184c98e70bdb79fb20d0ecd34553e9c121a9ba4f36760db4226 - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" @@ -20206,7 +20360,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -20252,6 +20406,13 @@ __metadata: languageName: node linkType: hard +"yoctocolors-cjs@npm:^2.1.2": + version: 2.1.2 + resolution: "yoctocolors-cjs@npm:2.1.2" + checksum: a0e36eb88fea2c7981eab22d1ba45e15d8d268626e6c4143305e2c1628fa17ebfaa40cd306161a8ce04c0a60ee0262058eab12567493d5eb1409780853454c6f + languageName: node + linkType: hard + "yoga-wasm-web@npm:~0.3.3": version: 0.3.3 resolution: "yoga-wasm-web@npm:0.3.3"