-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: bootstrap @fluentui/global-context package Autogenerated from `yarn create-package` * fix codeowners * update md * feat: Global context shim Adds a shim for `createContext` that will register the context to the global scope, and reuse a globally scoped context if it already exists * Add context selector shim and node + browser tests * update md * fix tsdoc * add bundlesize * Update packages/react-components/global-context/bundle-size/Default.fixture.js Co-authored-by: Oleksandr Fediashov <[email protected]> * custom webpack config * Update packages/react-components/global-context/bundle-size/createContextSelector.fixture.js Co-authored-by: Oleksandr Fediashov <[email protected]> * add README * remove react-theme * remove griffel Co-authored-by: Oleksandr Fediashov <[email protected]>
- Loading branch information
1 parent
e303ce6
commit 5090df2
Showing
17 changed files
with
329 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,13 @@ | ||
# @fluentui/global-context | ||
|
||
**Global Context components for [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)** | ||
**Global Context for [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)** | ||
|
||
These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. | ||
This package contains a shim for `React.createContext` API that will register the context object to the global | ||
scope (`window` for browsers, `global` for nodejs). This means that contexts will be real singletons. | ||
|
||
> ⚠️ The recommended approach is not to use this package and deduplicate affected packages in node_modules | ||
This package is is a workaround when multiple context objects are included into a bundle. This can happen when | ||
there are multiple copies of the same package installed in `node_modules`. | ||
|
||
**This package is not inteded to be used directly in code, but through a [Babel transform](/todo)** |
8 changes: 8 additions & 0 deletions
8
packages/react-components/global-context/bundle-size.config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// @ts-check | ||
|
||
module.exports = { | ||
webpack: (/** @type {import('webpack').Configuration} */ config) => { | ||
config.externals['@fluentui/react-context-selector'] = 'createContext'; | ||
return config; | ||
}, | ||
}; |
6 changes: 6 additions & 0 deletions
6
packages/react-components/global-context/bundle-size/createContext.fixture.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createContext } from '@fluentui/global-context'; | ||
console.log(createContext); | ||
|
||
export default { | ||
name: 'createContext', | ||
}; |
6 changes: 6 additions & 0 deletions
6
packages/react-components/global-context/bundle-size/createContextSelector.fixture.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { createContextSelector } from '@fluentui/global-context'; | ||
console.log(createContextSelector); | ||
|
||
export default { | ||
name: 'createContextSelector', | ||
}; |
61 changes: 61 additions & 0 deletions
61
packages/react-components/global-context/e2e/global-context.e2e.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { createContext, createContextSelector } from '@fluentui/global-context'; | ||
import { SYMBOL_NAMESPACE } from '../src/global-context'; | ||
import { SYMBOL_NAMESPACE as CONTEXT_SELECTOR_SYMBOL_NAMESPACE } from '../src/global-context-selector'; | ||
|
||
function cleanWindowSymbols(namespace: string) { | ||
getGlobalContextSymbols(namespace).forEach(sym => { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
delete window[sym]; | ||
}); | ||
} | ||
|
||
function getWindowProperty(symbol: Symbol) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
return window[symbol]; | ||
} | ||
|
||
function getGlobalContextSymbols(namespace: string) { | ||
return Object.getOwnPropertySymbols(global).reduce((acc, cur) => { | ||
if (Symbol.keyFor(cur)?.includes(namespace)) { | ||
acc.push(cur); | ||
} | ||
|
||
return acc; | ||
}, [] as Symbol[]); | ||
} | ||
|
||
describe('createContext', () => { | ||
beforeEach(() => cleanWindowSymbols(SYMBOL_NAMESPACE)); | ||
|
||
it('should create context on window', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
expect(getWindowProperty(getGlobalContextSymbols(SYMBOL_NAMESPACE)[0])).equals(MyContext); | ||
}); | ||
|
||
it('should create single context', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
const MyContext2 = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
|
||
expect(getGlobalContextSymbols(SYMBOL_NAMESPACE).length).equals(1); | ||
expect(getWindowProperty(getGlobalContextSymbols(SYMBOL_NAMESPACE)[0])).equals(MyContext); | ||
expect(MyContext2).equals(MyContext); | ||
}); | ||
}); | ||
|
||
describe('createContextSelector', () => { | ||
beforeEach(() => cleanWindowSymbols(CONTEXT_SELECTOR_SYMBOL_NAMESPACE)); | ||
|
||
it('should create context on window', () => { | ||
const MyContext = createContextSelector({}, 'MyContext', 'package', '9.0.0'); | ||
expect(getWindowProperty(getGlobalContextSymbols(CONTEXT_SELECTOR_SYMBOL_NAMESPACE)[0])).equals(MyContext); | ||
}); | ||
|
||
it('should create single context', () => { | ||
const MyContext = createContextSelector({}, 'MyContext', 'package', '9.0.0'); | ||
const MyContext2 = createContextSelector({}, 'MyContext', 'package', '9.0.0'); | ||
|
||
expect(getGlobalContextSymbols(CONTEXT_SELECTOR_SYMBOL_NAMESPACE).length).equals(1); | ||
expect(getWindowProperty(getGlobalContextSymbols(CONTEXT_SELECTOR_SYMBOL_NAMESPACE)[0])).equals(MyContext); | ||
expect(MyContext2).equals(MyContext); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../tsconfig.json", | ||
"compilerOptions": { | ||
"isolatedModules": false, | ||
"types": ["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"], | ||
"lib": ["ES2019", "dom"] | ||
}, | ||
"include": ["**/*.ts", "**/*.tsx"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
packages/react-components/global-context/src/global-context-selector.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* This suite intentionally runs in node environment to simulate SSR | ||
* @jest-environment node | ||
*/ | ||
|
||
import { createContext, SYMBOL_NAMESPACE } from './global-context-selector'; | ||
|
||
function getGlobalProperty(symbol: Symbol) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
return global[symbol]; | ||
} | ||
|
||
function getGlobalContextSymbols() { | ||
return Object.getOwnPropertySymbols(global).reduce((acc, cur) => { | ||
if (Symbol.keyFor(cur)?.includes(SYMBOL_NAMESPACE)) { | ||
acc.push(cur); | ||
} | ||
|
||
return acc; | ||
}, [] as Symbol[]); | ||
} | ||
|
||
describe('createContext', () => { | ||
beforeEach(() => { | ||
getGlobalContextSymbols().forEach(sym => { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
delete global[sym]; | ||
}); | ||
}); | ||
|
||
it('should create context on global', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
const sym = getGlobalContextSymbols()[0]; | ||
expect(getGlobalProperty(sym)).toBe(MyContext); | ||
}); | ||
|
||
it('should create single context', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
const MyContext2 = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
|
||
expect(getGlobalContextSymbols().length).toEqual(1); | ||
expect(getGlobalProperty(getGlobalContextSymbols()[0])).toBe(MyContext); | ||
expect(MyContext2).toBe(MyContext); | ||
}); | ||
}); |
53 changes: 53 additions & 0 deletions
53
packages/react-components/global-context/src/global-context-selector.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import * as React from 'react'; | ||
import { createContext as baseCreateContext } from '@fluentui/react-context-selector'; | ||
import { canUseDOM } from '@fluentui/react-utilities'; | ||
import { getMajorVersion } from './utils'; | ||
import { GlobalObject } from './types'; | ||
|
||
const isBrowser = canUseDOM(); | ||
const globalObject: GlobalObject = isBrowser ? window : global; | ||
|
||
// Identifier for the symbol, for easy idenfitifaction of symbols created by this util | ||
// Useful for clearning global object during SSR reloads | ||
export const SYMBOL_NAMESPACE = 'global-context-selector:'; | ||
|
||
// During SSR the global object persists with the server process | ||
// Clean out the global object during server reload during development | ||
if (!isBrowser && process.env.NODE_ENV !== 'production') { | ||
const globalSymbols = Object.getOwnPropertySymbols(globalObject); | ||
globalSymbols.forEach(sym => { | ||
if (Symbol.keyFor(sym)?.startsWith(SYMBOL_NAMESPACE)) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
delete globalObject[sym]; | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Wrapper around @see createContext from \@fluentui/react-context-selector that implements context registration | ||
* in the globalThis object to avoid duplicate contexts. Contexts are keyed with | ||
* a unique sybmol for the package name, version and name of the context. | ||
* | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol} | ||
* | ||
* @param defaultValue - @see React.createContext | ||
* @param name - name of the context | ||
* @param packageName - name of the npm package where the module is used | ||
* @param packageVersion - version of the npm package where the module is used | ||
* @returns @see React.createContext | ||
*/ | ||
export const createContext = <T>(defaultValue: T, name: string, packageName: string, packageVersion: string) => { | ||
// Symbol guaranteed to be unique for the entire runtime | ||
const sym = Symbol.for(`${SYMBOL_NAMESPACE}${packageName}/${name}/@${getMajorVersion(packageVersion)}`); | ||
|
||
// Objects keyed with symbols are not visible with console.log | ||
// Object symbol properties can't be iterated with `for` or `Object.keys` | ||
const globalSymbols = Object.getOwnPropertySymbols(globalObject); | ||
if (!globalSymbols.includes(sym)) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
globalObject[sym] = baseCreateContext(defaultValue); | ||
} | ||
|
||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
return globalObject[sym] as React.Context<T>; | ||
}; |
45 changes: 45 additions & 0 deletions
45
packages/react-components/global-context/src/global-context.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* This suite intentionally runs in node environment to simulate SSR | ||
* @jest-environment node | ||
*/ | ||
|
||
import { createContext, SYMBOL_NAMESPACE } from './global-context'; | ||
|
||
function getGlobalProperty(symbol: Symbol) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
return global[symbol]; | ||
} | ||
|
||
function getGlobalContextSymbols() { | ||
return Object.getOwnPropertySymbols(global).reduce((acc, cur) => { | ||
if (Symbol.keyFor(cur)?.includes(SYMBOL_NAMESPACE)) { | ||
acc.push(cur); | ||
} | ||
|
||
return acc; | ||
}, [] as Symbol[]); | ||
} | ||
|
||
describe('createContext', () => { | ||
beforeEach(() => { | ||
getGlobalContextSymbols().forEach(sym => { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
delete global[sym]; | ||
}); | ||
}); | ||
|
||
it('should create context on global', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
const sym = getGlobalContextSymbols()[0]; | ||
expect(getGlobalProperty(sym)).toBe(MyContext); | ||
}); | ||
|
||
it('should create single context', () => { | ||
const MyContext = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
const MyContext2 = createContext({}, 'MyContext', 'package', '9.0.0'); | ||
|
||
expect(getGlobalContextSymbols().length).toEqual(1); | ||
expect(getGlobalProperty(getGlobalContextSymbols()[0])).toBe(MyContext); | ||
expect(MyContext2).toBe(MyContext); | ||
}); | ||
}); |
52 changes: 52 additions & 0 deletions
52
packages/react-components/global-context/src/global-context.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import * as React from 'react'; | ||
import { canUseDOM } from '@fluentui/react-utilities'; | ||
import { GlobalObject } from './types'; | ||
import { getMajorVersion } from './utils'; | ||
|
||
const isBrowser = canUseDOM(); | ||
const globalObject: GlobalObject = isBrowser ? window : global; | ||
|
||
// Identifier for the symbol, for easy idenfitifaction of symbols created by this util | ||
// Useful for clearning global object during SSR reloads | ||
export const SYMBOL_NAMESPACE = 'global-context:'; | ||
|
||
// During SSR the global object persists with the server process | ||
// Clean out the global object during server reload during development | ||
if (!isBrowser && process.env.NODE_ENV !== 'production') { | ||
const globalSymbols = Object.getOwnPropertySymbols(globalObject); | ||
globalSymbols.forEach(sym => { | ||
if (Symbol.keyFor(sym)?.startsWith(SYMBOL_NAMESPACE)) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
delete globalObject[sym]; | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Wrapper around @see React.createContext that implements context registration | ||
* in the globalThis object to avoid duplicate contexts. Contexts are keyed with | ||
* a unique sybmol for the package name, version and name of the context. | ||
* | ||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol} | ||
* | ||
* @param defaultValue - @see React.createContext | ||
* @param name - name of the context | ||
* @param packageName - name of the npm package where the module is used | ||
* @param packageVersion - version of the npm package where the module is used | ||
* @returns @see React.createContext | ||
*/ | ||
export const createContext = <T>(defaultValue: T, name: string, packageName: string, packageVersion: string) => { | ||
// Symbol guaranteed to be unique for the entire runtime | ||
const sym = Symbol.for(`${SYMBOL_NAMESPACE}${packageName}/${name}/@${getMajorVersion(packageVersion)}`); | ||
|
||
// Objects keyed with symbols are not visible with console.log | ||
// Object symbol properties can't be iterated with `for` or `Object.keys` | ||
const globalSymbols = Object.getOwnPropertySymbols(globalObject); | ||
if (!globalSymbols.includes(sym)) { | ||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
globalObject[sym] = React.createContext(defaultValue); | ||
} | ||
|
||
// @ts-expect-error - Indexing object with symbols not supported until TS 4.4 | ||
return globalObject[sym] as React.Context<T>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
// TODO: replace with real exports | ||
export {}; | ||
export { createContext } from './global-context'; | ||
export { createContext as createContextSelector } from './global-context-selector'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import * as React from 'react'; | ||
|
||
export type GlobalObject = (typeof globalThis | NodeJS.Global) & Record<symbol, React.Context<unknown>>; |
10 changes: 10 additions & 0 deletions
10
packages/react-components/global-context/src/utils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { getMajorVersion } from './utils'; | ||
describe('getMajorVersion', () => { | ||
it.each([ | ||
['9.0.0', 9], | ||
['9.0.0-rc.1', 9], | ||
['9.0.0-rc.1+buildNumber', 9], | ||
])('should get major version number from %s', (version, majorVersion) => { | ||
expect(getMajorVersion(version)).toBe(majorVersion); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* @param version - semver version string | ||
* @returns The major version number | ||
*/ | ||
export function getMajorVersion(version: string) { | ||
return Number(version.split('.')[0]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters