Skip to content

Commit 60d24da

Browse files
authored
chore: improve @magicbell/react-headless cjs/esm support (#391)
1 parent c47faa4 commit 60d24da

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+237
-162
lines changed

.changeset/empty-walls-turn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@magicbell/react-headless': minor
3+
---
4+
5+
fix cjs/esm dual module support

jest.config.cjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const commonConfig = {
2727
tsconfig: 'tsconfig.test.json',
2828
}]
2929
},
30+
resolver: './jest.resolver.cjs',
31+
moduleFileExtensions: ['ts', 'tsx', 'cts', 'js', 'json'],
3032
modulePathIgnorePatterns: ['<rootDir>/packages/magicbell/dist', '<rootDir>/packages/playground', '<rootDir>/packages/embeddable/cypress'],
3133
globals: {
3234
__PACKAGE_NAME__: 'TEST',
@@ -41,16 +43,22 @@ const commonConfig = {
4143
moduleNameMapper,
4244
};
4345

46+
const projectConfigs = {
47+
'@magicbell/user-client': {
48+
testEnvironment: 'node',
49+
}
50+
};
51+
4452
/** @type {import('jest').Config} */
4553
module.exports = {
4654
projects: packages.map(([name, dir]) => ({
4755
...commonConfig,
4856
displayName: name,
49-
testEnvironment: name === '@magicbell/user-client' ? 'node' : "jest-environment-jsdom",
5057
testMatch: [
5158
`${dir}/src/**/*.test.[jt]s?(x)"`,
5259
`${dir}/test/**/*.[jt]s?(x)"`,
5360
`${dir}/tests/**/*.spec.[jt]s?(x)"`,
5461
],
62+
...projectConfigs[name],
5563
}))
5664
};

jest.resolver.cjs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fs = require('fs');
2+
3+
module.exports = (request, options) => {
4+
const { defaultResolver } = options;
5+
const resolvedPath = defaultResolver(request, options);
6+
7+
// try resolve tshy module resolution polyfills
8+
const cjsPath = resolvedPath.replace(/\.ts$/, '-cjs.cts');
9+
if (fs.existsSync(cjsPath)) return cjsPath;
10+
11+
return resolvedPath;
12+
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"eslint-plugin-react": "^7.34.0",
7878
"eslint-plugin-react-hooks": "^4.6.0",
7979
"eslint-plugin-simple-import-sort": "^8.0.0",
80-
"fix-esm-import-path": "^1.10.0",
80+
"fix-esm-import-path": "^1.10.1",
8181
"happy-dom": "^15.7.3",
8282
"husky": "^9.0.11",
8383
"identity-obj-proxy": "^3.0.0",
@@ -90,6 +90,7 @@
9090
"prettier": "^2.8.8",
9191
"react": "^18.3.1",
9292
"react-dom": "^18.3.1",
93+
"replace-in-file": "^8.2.0",
9394
"rimraf": "^5.0.7",
9495
"rollup-plugin-analyzer": "^4.0.0",
9596
"size-limit": "^8.2.6",

packages/react-headless/package.json

+29-21
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
"Stephan Meijer <[email protected]>"
1010
],
1111
"license": "SEE LICENSE IN LICENSE",
12-
"source": "./src/index.ts",
13-
"main": "dist/index.js",
14-
"module": "dist/magicbell-react-headless.esm.js",
15-
"typings": "dist/index.d.ts",
1612
"sideEffects": false,
1713
"files": [
1814
"/dist",
@@ -42,22 +38,17 @@
4238
},
4339
"scripts": {
4440
"clean": "rimraf dist",
45-
"build": "run-s clean build:*",
46-
"build:dev": "vite build -c ../../scripts/vite/vite.config.js",
47-
"build:prod": "vite build -c ../../scripts/vite/vite.config.js --minify",
48-
"start": "yarn build:dev --watch",
49-
"size": "size-limit"
41+
"build": "run-s clean build:bundle build:replace test:bundle build:attw",
42+
"build:bundle": "tshy",
43+
"build:replace": "tsx scripts/post-build.ts",
44+
"build:attw": "attw -P",
45+
"test:bundle": "node tests/bundle/commonjs.cjs && node tests/bundle/esm.mjs",
46+
"start": "tshy --watch"
47+
},
48+
"tshy": {
49+
"project": "./tsconfig.build.json",
50+
"exports": "./src/*.ts"
5051
},
51-
"size-limit": [
52-
{
53-
"path": "dist/magicbell-react-headless.cjs.min.js",
54-
"limit": "125 KB"
55-
},
56-
{
57-
"path": "dist/magicbell-react-headless.esm.min.js",
58-
"limit": "125 KB"
59-
}
60-
],
6152
"devDependencies": {
6253
"@faker-js/faker": "^6.3.1",
6354
"@size-limit/preset-small-lib": "^11.1.4",
@@ -77,7 +68,7 @@
7768
"dependencies": {
7869
"dayjs": "^1.11.10",
7970
"humps": "^2.0.1",
80-
"immer": "^9.0.21",
71+
"immer": "^10.1.1",
8172
"lodash": "^4.17.21",
8273
"lodash-es": "^4.17.21",
8374
"magicbell": "4.1.0",
@@ -90,5 +81,22 @@
9081
},
9182
"peerDependencies": {
9283
"react": ">= 18.3.1"
93-
}
84+
},
85+
"type": "module",
86+
"exports": {
87+
".": {
88+
"import": {
89+
"types": "./dist/esm/index.d.ts",
90+
"default": "./dist/esm/index.js"
91+
},
92+
"require": {
93+
"types": "./dist/commonjs/index.d.ts",
94+
"default": "./dist/commonjs/index.js"
95+
}
96+
},
97+
"./package.json": "./package.json"
98+
},
99+
"module": "./dist/esm/index.js",
100+
"main": "./dist/commonjs/index.js",
101+
"types": "./dist/commonjs/index.d.ts"
94102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { replaceInFile, ReplaceInFileConfig } from 'replace-in-file';
2+
3+
import pkgJson from '../package.json';
4+
import { pkg } from '../src/lib/pkg';
5+
6+
const config = {
7+
files: 'dist/*/lib/pkg.js',
8+
from: ['__PACKAGE_NAME__', '__PACKAGE_VERSION__'],
9+
to: [pkgJson.name, pkgJson.version],
10+
} satisfies ReplaceInFileConfig;
11+
12+
for (const key of Object.values(pkg)) {
13+
if (!config.from.includes(key)) {
14+
process.stdout.write(`Unknown replacement key '${key}' in lib/pkg.ts\n`);
15+
process.exit(1);
16+
}
17+
}
18+
19+
await replaceInFile(config);

packages/react-headless/src/components/MagicBellProvider/MagicBellProvider.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { useEffect, useState } from 'react';
22

3-
import clientSettings, { ClientSettings } from '../../stores/clientSettings';
4-
import useConfig from '../../stores/config';
5-
import { useNotificationStoresCollection } from '../../stores/notifications';
6-
import buildStore from '../../stores/notifications/helpers/buildStore';
7-
import { QueryParams } from '../../types/INotificationsStoresCollection';
8-
import INotificationStore from '../../types/INotificationStore';
9-
import RealtimeListener from '../RealtimeListener';
3+
import clientSettings, { ClientSettings } from '../../stores/clientSettings.js';
4+
import useConfig from '../../stores/config/index.js';
5+
import buildStore from '../../stores/notifications/helpers/buildStore.js';
6+
import { useNotificationStoresCollection } from '../../stores/notifications/index.js';
7+
import { QueryParams } from '../../types/INotificationsStoresCollection.js';
8+
import INotificationStore from '../../types/INotificationStore.js';
9+
import RealtimeListener from '../RealtimeListener.js';
1010

1111
type StoreConfig = {
1212
id: string;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import MagicBellProvider from './MagicBellProvider';
1+
import MagicBellProvider from './MagicBellProvider.js';
22

3-
export type { MagicBellProviderProps } from './MagicBellProvider';
3+
export type { MagicBellProviderProps } from './MagicBellProvider.js';
44
export default MagicBellProvider;

packages/react-headless/src/components/RealtimeListener.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useEffect } from 'react';
22

3-
import useMagicBellEvent from '../hooks/useMagicBellEvent';
4-
import { handleAblyEvent } from '../lib/realtime';
5-
import clientSettings from '../stores/clientSettings';
6-
import { useNotificationStoresCollection } from '../stores/notifications';
7-
import IRemoteNotification from '../types/IRemoteNotification';
3+
import useMagicBellEvent from '../hooks/useMagicBellEvent.js';
4+
import { handleAblyEvent } from '../lib/realtime.js';
5+
import clientSettings from '../stores/clientSettings.js';
6+
import { useNotificationStoresCollection } from '../stores/notifications/index.js';
7+
import IRemoteNotification from '../types/IRemoteNotification.js';
88

99
/**
1010
* Component that setups a listener to realtime events and keeps notifications

packages/react-headless/src/components/WebPushNotificationsSubscriber/WebPushNotificationsSubscriber.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { path } from 'ramda';
22
import { useEffect } from 'react';
33

4-
import { createPushSubscription, createSafariPushSubscription } from '../../lib/push';
5-
import useConfig from '../../stores/config';
4+
import { createPushSubscription, createSafariPushSubscription } from '../../lib/push.js';
5+
import useConfig from '../../stores/config/index.js';
66

77
export interface Props {
88
children: (params: { createSubscription: () => Promise<unknown>; isPushAPISupported: boolean }) => JSX.Element;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import WebPushNotificationsSubscriber from './WebPushNotificationsSubscriber';
1+
import WebPushNotificationsSubscriber from './WebPushNotificationsSubscriber.js';
22

3-
export type { Props as WebPushNotificationsSubscriberProps } from './WebPushNotificationsSubscriber';
3+
export type { Props as WebPushNotificationsSubscriberProps } from './WebPushNotificationsSubscriber.js';
44
export default WebPushNotificationsSubscriber;

packages/react-headless/src/hooks/useBell.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import useNotifications, { NotificationStore } from './useNotifications';
1+
import useNotifications, { NotificationStore } from './useNotifications.js';
22

33
interface useBellArgs {
44
storeId?: string;

packages/react-headless/src/hooks/useMagicBellEvent.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect } from 'react';
22

3-
import { eventAggregator, EventSource } from '../lib/realtime';
3+
import { eventAggregator, EventSource } from '../lib/realtime.js';
44

55
interface HookOptions {
66
source: EventSource | 'any';

packages/react-headless/src/hooks/useNotification.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import INotification from '../types/INotification';
2-
import IRemoteNotification from '../types/IRemoteNotification';
3-
import useNotificationFactory from './useNotificationFactory';
4-
import useNotificationUnmount from './useNotificationUnmount';
1+
import INotification from '../types/INotification.js';
2+
import IRemoteNotification from '../types/IRemoteNotification.js';
3+
import useNotificationFactory from './useNotificationFactory.js';
4+
import useNotificationUnmount from './useNotificationUnmount.js';
55

66
export default function useNotification(
77
data: IRemoteNotification,

packages/react-headless/src/hooks/useNotificationFactory.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { isNil } from 'ramda';
22

3-
import { secondsToDate } from '../lib/date';
4-
import { parseJSON } from '../lib/json';
5-
import { useNotificationStoresCollection } from '../stores/notifications';
6-
import INotification from '../types/INotification';
7-
import IRemoteNotification from '../types/IRemoteNotification';
3+
import { secondsToDate } from '../lib/date.js';
4+
import { parseJSON } from '../lib/json.js';
5+
import { useNotificationStoresCollection } from '../stores/notifications/index.js';
6+
import INotification from '../types/INotification.js';
7+
import IRemoteNotification from '../types/IRemoteNotification.js';
88

99
/**
1010
* Hook that builds a notification object.

packages/react-headless/src/hooks/useNotificationUnmount.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect } from 'react';
22

3-
import INotification from '../types/INotification';
3+
import INotification from '../types/INotification.js';
44

55
/**
66
* Hook that is ran when the component is unmounted. By default marks a

packages/react-headless/src/hooks/useNotifications.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useCallback, useEffect } from 'react';
22

3-
import useConfig from '../stores/config';
4-
import { useNotificationStoresCollection } from '../stores/notifications';
5-
import { QueryParams } from '../types/INotificationsStoresCollection';
6-
import INotificationStore from '../types/INotificationStore';
3+
import useConfig from '../stores/config/index.js';
4+
import { useNotificationStoresCollection } from '../stores/notifications/index.js';
5+
import { QueryParams } from '../types/INotificationsStoresCollection.js';
6+
import INotificationStore from '../types/INotificationStore.js';
77

88
type FetchOptions = Partial<{
99
reset: boolean;

packages/react-headless/src/index.ts

+19-33
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,19 @@
1-
import warning from 'tiny-warning';
2-
3-
export { default as MagicBellProvider } from './components/MagicBellProvider';
4-
export { default as RealtimeListener } from './components/RealtimeListener';
5-
export { default as WebPushNotificationsSubscriber } from './components/WebPushNotificationsSubscriber';
6-
export { default as useBell } from './hooks/useBell';
7-
export { default as useMagicBellEvent } from './hooks/useMagicBellEvent';
8-
export { default as useNotification } from './hooks/useNotification';
9-
export { default as useNotificationFactory } from './hooks/useNotificationFactory';
10-
export { default as useNotifications } from './hooks/useNotifications';
11-
export { default as useNotificationUnmount } from './hooks/useNotificationUnmount';
12-
export { deleteAPI, fetchAPI, postAPI, putAPI } from './lib/ajax';
13-
export { secondsToDate, toDate, toUnix } from './lib/date';
14-
export { eventAggregator, pushEventAggregator } from './lib/realtime';
15-
export { default as clientSettings } from './stores/clientSettings';
16-
export { default as useConfig } from './stores/config';
17-
export { default as useNotificationPreferences } from './stores/notification_preferences';
18-
export { useNotificationStoresCollection } from './stores/notifications';
19-
export { default as buildStore } from './stores/notifications/helpers/buildStore';
20-
export * from './types';
21-
export { type INotification as Notification } from './types';
22-
23-
if (__DEV__) {
24-
// eslint-disable-next-line @typescript-eslint/no-empty-function
25-
const testFunc = function testFn() {};
26-
27-
warning(
28-
(testFunc.name || testFunc.toString()).indexOf('testFn') !== -1,
29-
"It looks like you're using a minified copy of the development build " +
30-
`of ${__PACKAGE_NAME__}. When deploying your app to production, make sure to use ` +
31-
'the production build which is faster and does not print development warnings.',
32-
);
33-
}
1+
export { default as MagicBellProvider } from './components/MagicBellProvider/index.js';
2+
export { default as RealtimeListener } from './components/RealtimeListener.js';
3+
export { default as WebPushNotificationsSubscriber } from './components/WebPushNotificationsSubscriber/index.js';
4+
export { default as useBell } from './hooks/useBell.js';
5+
export { default as useMagicBellEvent } from './hooks/useMagicBellEvent.js';
6+
export { default as useNotification } from './hooks/useNotification.js';
7+
export { default as useNotificationFactory } from './hooks/useNotificationFactory.js';
8+
export { default as useNotifications } from './hooks/useNotifications.js';
9+
export { default as useNotificationUnmount } from './hooks/useNotificationUnmount.js';
10+
export { deleteAPI, fetchAPI, postAPI, putAPI } from './lib/ajax.js';
11+
export { secondsToDate, toDate, toUnix } from './lib/date.js';
12+
export { eventAggregator, pushEventAggregator } from './lib/realtime.js';
13+
export { default as clientSettings } from './stores/clientSettings.js';
14+
export { default as useConfig } from './stores/config/index.js';
15+
export { default as useNotificationPreferences } from './stores/notification_preferences/index.js';
16+
export { default as buildStore } from './stores/notifications/helpers/buildStore.js';
17+
export { useNotificationStoresCollection } from './stores/notifications/index.js';
18+
export * from './types/index.js';
19+
export { type INotification as Notification } from './types/index.js';

packages/react-headless/src/lib/ajax.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import clientSettings from '../stores/clientSettings';
1+
import clientSettings from '../stores/clientSettings.js';
22

33
/**
44
* Performs an ajax request to the MagicBell API server.

packages/react-headless/src/lib/date.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dayjs, { type Dayjs } from 'dayjs';
2-
import localizedFormat from 'dayjs/plugin/localizedFormat';
3-
import relativeTime from 'dayjs/plugin/relativeTime';
4-
import updateLocale from 'dayjs/plugin/updateLocale';
2+
import localizedFormat from 'dayjs/plugin/localizedFormat.js';
3+
import relativeTime from 'dayjs/plugin/relativeTime.js';
4+
import updateLocale from 'dayjs/plugin/updateLocale.js';
55

66
dayjs.extend(localizedFormat);
77
dayjs.extend(relativeTime);
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Note, constants are replaced right after tsc built the project.
2+
// Update scripts/post-build.ts when requirements here change.
3+
export const pkg = {
4+
name: '__PACKAGE_NAME__',
5+
version: '__PACKAGE_VERSION__',
6+
};

packages/react-headless/src/lib/push.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { path } from 'ramda';
22

3-
import { IRemoteConfig } from '../types';
4-
import { postAPI } from './ajax';
3+
import { IRemoteConfig } from '../types/index.js';
4+
import { postAPI } from './ajax.js';
55

66
function stringToUint8Array(plainString: string) {
77
const padding = '='.repeat((4 - (plainString.length % 4)) % 4);

packages/react-headless/src/lib/realtime.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import mitt from 'mitt';
2-
3-
import clientSettings from '../stores/clientSettings';
4-
import NotificationRepository from '../stores/notifications/NotificationRepository';
1+
import { mitt } from '../polyfills/mitt-module.js';
2+
import clientSettings from '../stores/clientSettings.js';
3+
import NotificationRepository from '../stores/notifications/NotificationRepository.js';
4+
import { pkg } from './pkg.js';
55

66
export function getAuthHeaders() {
77
const { apiKey, userEmail, userExternalId, userKey } = clientSettings.getState();
88

99
const headers = {
1010
'x-magicbell-api-key': apiKey,
11-
'x-magicbell-client-user-agent': `${__PACKAGE_NAME__}/${__PACKAGE_VERSION__}`,
11+
'x-magicbell-client-user-agent': `${pkg.name}/${pkg.version}`,
1212
};
1313

1414
if (userEmail) headers['x-magicbell-user-email'] = userEmail;

0 commit comments

Comments
 (0)