diff --git a/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html b/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html new file mode 100644 index 0000000000..fa68c5d8d8 --- /dev/null +++ b/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html @@ -0,0 +1,95 @@ + + +
+ + + + + + + + + + + + + + + diff --git a/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html.snap-1.png b/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html.snap-1.png new file mode 100644 index 0000000000..ab29e8bcb9 Binary files /dev/null and b/__tests__/html2/hooks/useStyles/cssModuleTheme.fluent.html.snap-1.png differ diff --git a/packages/bundle/.eslintrc.yml b/packages/bundle/.eslintrc.yml index 120e30c270..a4936c5bda 100644 --- a/packages/bundle/.eslintrc.yml +++ b/packages/bundle/.eslintrc.yml @@ -14,3 +14,6 @@ rules: - group: - '@msinternal/botframework-webchat-api-*' message: 'To prevent duplicated import, @msinternal/botframework-webchat-api-* is only allowed to import in botframework-webchat-api.' + paths: + - name: '@msinternal/botframework-webchat-styles/react' + message: 'Every copy of @msinternal/botframework-webchat-styles/react has its own React context, import it from botframework-webchat-component/internal.js instead.' diff --git a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx index 5e6aa6a763..53e40b6b8c 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx @@ -1,11 +1,10 @@ /* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 2] }] */ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { type Action as AdaptiveCardAction, type OpenUrlAction, type SubmitAction } from 'adaptivecards'; import { ErrorBoxPolymiddlewareProxy } from 'botframework-webchat-api/middleware.js'; import { getTabIndex, hooks } from 'botframework-webchat-component'; -import { useQueueStaticElement } from 'botframework-webchat-component/internal.js'; +import { useQueueStaticElement, useStyles } from 'botframework-webchat-component/internal.js'; import { type DirectLineCardAction } from 'botframework-webchat-core'; import cx from 'classnames'; import React, { diff --git a/packages/bundle/src/adaptiveCards/Attachment/AnimatedImageContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/AnimatedImageContent.tsx index e16d4fd3e8..2582bf19a1 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AnimatedImageContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AnimatedImageContent.tsx @@ -1,7 +1,6 @@ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { hooks } from 'botframework-webchat-component'; -import { ActivityButton, FixedWidthImage } from 'botframework-webchat-component/internal.js'; +import { ActivityButton, FixedWidthImage, useStyles } from 'botframework-webchat-component/internal.js'; import cx from 'classnames'; import React, { memo, useCallback, useRef, useState, type SyntheticEvent } from 'react'; import { boolean, object, optional, pipe, readonly, string } from 'valibot'; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx index 4b05aef877..7a8488c557 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx @@ -1,8 +1,8 @@ /* eslint react/no-array-index-key: "off" */ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; +import { useStyles } from 'botframework-webchat-component/internal.js'; import { onErrorResumeNext } from 'botframework-webchat-core'; import React, { memo } from 'react'; import { boolean, object, optional, pipe, readonly, string } from 'valibot'; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx index 4be222e319..5b90cb61ef 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx @@ -1,8 +1,8 @@ /* eslint react/no-array-index-key: "off" */ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; +import { useStyles } from 'botframework-webchat-component/internal.js'; import React, { memo } from 'react'; import { boolean, object, optional, pipe, readonly, string } from 'valibot'; diff --git a/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx index 57887dc9ff..05f8512a2b 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx @@ -1,5 +1,5 @@ import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; +import { useStyles } from 'botframework-webchat-component/internal.js'; import React, { memo } from 'react'; import { boolean, object, optional, pipe, readonly, string } from 'valibot'; diff --git a/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx index 42645da61c..c3cb99460c 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx @@ -1,8 +1,8 @@ /* eslint react/no-array-index-key: "off" */ import type { InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; +import { useStyles } from 'botframework-webchat-component/internal.js'; import React, { memo } from 'react'; import { boolean, object, optional, parse, pipe, readonly, string } from 'valibot'; diff --git a/packages/bundle/src/boot/actual/internal.ts b/packages/bundle/src/boot/actual/internal.ts index fc8c411144..36614f5d2f 100644 --- a/packages/bundle/src/boot/actual/internal.ts +++ b/packages/bundle/src/boot/actual/internal.ts @@ -2,7 +2,9 @@ export { usePostVoiceActivity, useShouldShowMicrophoneButton } from 'botframework-webchat-api/internal.js'; export { CodeHighlighterComposer, + combineCSSModules, createIconComponent, + CSSModuleThemeComposer, InjectStyleElements, injectStyleElementsPropsSchema, parseDocumentFragmentFromString, @@ -11,7 +13,8 @@ export { serializeDocumentFragmentIntoString, useActivityStyleOptions, useCodeHighlighter, - useLiveRegion + useLiveRegion, + useStyles } from 'botframework-webchat-component/internal.js'; export { getOrgSchemaMessage, diff --git a/packages/component/src/boot/internal.ts b/packages/component/src/boot/internal.ts index 2faa3f1041..521c84a81f 100644 --- a/packages/component/src/boot/internal.ts +++ b/packages/component/src/boot/internal.ts @@ -5,7 +5,15 @@ export { type BetterLinkDocumentModAttributeSetter, type BetterLinkDocumentModDecoration } from '@msinternal/botframework-webchat-component-better-link'; -export { InjectStyleElements, injectStyleElementsPropsSchema } from '@msinternal/botframework-webchat-styles/react'; +// The styles package is bundled into every package that imports it, and each copy has its own React context. +// Other packages must use this copy, so a theme above Web Chat reaches every useStyles(). +export { + combineCSSModules, + CSSModuleThemeComposer, + InjectStyleElements, + injectStyleElementsPropsSchema, + useStyles +} from '@msinternal/botframework-webchat-styles/react'; export { default as ActivityButton } from '../Attachment/Text/private/ActivityButton'; export { CodeHighlighterComposer, diff --git a/packages/fluent-theme/.eslintrc.yml b/packages/fluent-theme/.eslintrc.yml index 8307709bdc..194d6ebbb3 100644 --- a/packages/fluent-theme/.eslintrc.yml +++ b/packages/fluent-theme/.eslintrc.yml @@ -7,5 +7,10 @@ env: browser: true rules: + no-restricted-imports: + - error + - paths: + - name: '@msinternal/botframework-webchat-styles/react' + message: 'Every copy of @msinternal/botframework-webchat-styles/react has its own React context, import it from botframework-webchat/internal.js instead.' react/destructuring-assignment: off react/require-default-props: off diff --git a/packages/fluent-theme/src/__tests__/sharedStylesRuntime.spec.js b/packages/fluent-theme/src/__tests__/sharedStylesRuntime.spec.js new file mode 100644 index 0000000000..079f01305e --- /dev/null +++ b/packages/fluent-theme/src/__tests__/sharedStylesRuntime.spec.js @@ -0,0 +1,84 @@ +/** @jest-environment @happy-dom/jest-environment */ + +// Every copy of the styles runtime has its own CSSModuleThemeContext: a theme provided through one copy is invisible to +// useStyles() of another copy. Web Chat must ship one copy, from botframework-webchat-component. + +const { readdirSync, readFileSync } = require('fs'); +const { dirname, join } = require('path'); + +// Only the styles runtime names its context, minifiers keep string literals. +const STYLES_RUNTIME_MARKER = 'WebChat.CSSModuleThemeContext'; + +// Lists the build outputs that bundle the styles runtime, per format, from esbuild metafiles (tsup `metafile: true`). +function outputsWithStylesRuntime(packageName) { + const distPath = dirname(require.resolve(packageName)); + + return Object.fromEntries( + // Paths come from require.resolve() and the dist folder listing, not from user input. + // eslint-disable-next-line security/detect-non-literal-fs-filename + readdirSync(distPath) + .filter(name => /^metafile-\w+\.json$/u.test(name)) + .map(name => [ + name, + // eslint-disable-next-line security/detect-non-literal-fs-filename + Object.entries(JSON.parse(readFileSync(join(distPath, name), 'utf8')).outputs) + .filter(([, { inputs }]) => + Object.keys(inputs).some(input => input.includes('styles/dist/botframework-webchat-styles.react.')) + ) + .map(([output]) => output) + ]) + ); +} + +test('botframework-webchat/internal.js should re-export the styles runtime of botframework-webchat-component', () => { + const bundleInternal = require('botframework-webchat/internal.js'); + const componentInternal = require('botframework-webchat-component/internal.js'); + + expect(bundleInternal.CSSModuleThemeComposer).toBe(componentInternal.CSSModuleThemeComposer); + expect(bundleInternal.useStyles).toBe(componentInternal.useStyles); +}); + +test('botframework-webchat-component should bundle the styles runtime into one chunk per module format', () => { + const outputs = outputsWithStylesRuntime('botframework-webchat-component'); + + expect(Object.keys(outputs).sort()).toEqual(['metafile-cjs.json', 'metafile-esm.json']); + expect(outputs['metafile-cjs.json']).toHaveLength(1); + expect(outputs['metafile-esm.json']).toHaveLength(1); + + // The marker used by the botframework-webchat-fluent-theme test below is in the styles runtime chunks. + [...outputs['metafile-cjs.json'], ...outputs['metafile-esm.json']].forEach(output => + expect( + // Paths come from the esbuild metafile, not from user input. + // eslint-disable-next-line security/detect-non-literal-fs-filename + readFileSync(join(dirname(require.resolve('botframework-webchat-component')), '..', output), 'utf8') + ).toContain(STYLES_RUNTIME_MARKER) + ); +}); + +test('botframework-webchat should not bundle its own copy of the styles runtime', () => + // IIFE builds (webchat.js) contain one copy, inside the botframework-webchat-component code they inline. + expect(outputsWithStylesRuntime('botframework-webchat')).toEqual({ + 'metafile-cjs.json': [], + 'metafile-esm.json': [], + 'metafile-iife.json': [] + })); + +test('botframework-webchat-fluent-theme should not bundle the styles runtime', () => { + // The package does not publish its esbuild metafiles ("files" in package.json), so check the published scripts. + const distPath = dirname(require.resolve('botframework-webchat-fluent-theme')); + const scripts = readdirSync(distPath).filter(name => /\.m?js$/u.test(name)); + + // CommonJS, ES modules, and the development and production IIFE builds; no chunks. + expect(scripts.sort()).toEqual([ + 'botframework-webchat-fluent-theme.development.js', + 'botframework-webchat-fluent-theme.js', + 'botframework-webchat-fluent-theme.mjs', + 'botframework-webchat-fluent-theme.production.min.js' + ]); + expect( + scripts.filter(name => + // eslint-disable-next-line security/detect-non-literal-fs-filename + readFileSync(join(distPath, name), 'utf8').includes(STYLES_RUNTIME_MARKER) + ) + ).toEqual([]); +}); diff --git a/packages/fluent-theme/src/components/activity/ActivityLoader.tsx b/packages/fluent-theme/src/components/activity/ActivityLoader.tsx index 3c2348f1ef..2e844a2092 100644 --- a/packages/fluent-theme/src/components/activity/ActivityLoader.tsx +++ b/packages/fluent-theme/src/components/activity/ActivityLoader.tsx @@ -1,10 +1,9 @@ import { reactNode, validateProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import cx from 'classnames'; import React, { Fragment, memo } from 'react'; import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; -import { useVariantClassName } from '../../styles'; +import { useStyles, useVariantClassName } from '../../styles'; import SlidingDots from '../assets/SlidingDots'; import styles from './ActivityLoader.module.css'; diff --git a/packages/fluent-theme/src/components/icon/FluentIcon.tsx b/packages/fluent-theme/src/components/icon/FluentIcon.tsx index 416796237a..48ee4d1451 100644 --- a/packages/fluent-theme/src/components/icon/FluentIcon.tsx +++ b/packages/fluent-theme/src/components/icon/FluentIcon.tsx @@ -1,10 +1,10 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { createIconComponent } from 'botframework-webchat/internal.js'; import cx from 'classnames'; import React, { memo, useMemo, type CSSProperties } from 'react'; import { object, optional, pipe, readonly, string, union, type InferInput } from 'valibot'; +import { useStyles } from '../../styles'; import styles from './FluentIcon.module.css'; const baseFluentIconPropsSchema = pipe( diff --git a/packages/fluent-theme/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx b/packages/fluent-theme/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx index 6e35a5022b..62cb72f039 100644 --- a/packages/fluent-theme/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx +++ b/packages/fluent-theme/src/components/typingIndicator/SlidingDotsTypingIndicator.tsx @@ -1,8 +1,8 @@ -import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { testIds } from 'botframework-webchat'; import cx from 'classnames'; import React, { memo } from 'react'; +import { useStyles } from '../../styles'; import SlidingDots from '../assets/SlidingDots'; import styles from './SlidingDotsTypingIndicator.module.css'; diff --git a/packages/fluent-theme/src/styles/useStyles.ts b/packages/fluent-theme/src/styles/useStyles.ts index 974db3840f..d5c1e00f0d 100644 --- a/packages/fluent-theme/src/styles/useStyles.ts +++ b/packages/fluent-theme/src/styles/useStyles.ts @@ -1,2 +1,2 @@ -// TODO: refactor after initial approval gained -export { useStyles as default } from '@msinternal/botframework-webchat-styles/react'; +// Use the copy from Web Chat, so Fluent elements read the same CSSModuleThemeContext as Web Chat elements. +export { useStyles as default } from 'botframework-webchat/internal.js'; diff --git a/packages/styles/src/react/index.ts b/packages/styles/src/react/index.ts index 692bb72103..ac3668c1e8 100644 --- a/packages/styles/src/react/index.ts +++ b/packages/styles/src/react/index.ts @@ -1,2 +1,10 @@ +export { localClassName, webChatPrefixedClassName, type ClassNameTransform } from './private/classNameTransforms'; +export { default as combineCSSModules } from './private/combineCSSModules'; +export { + default as CSSModuleThemeComposer, + cssModuleThemeComposerPropsSchema, + type CSSModuleThemeComposerProps +} from './private/CSSModuleThemeComposer'; +export { type CSSModuleTheme } from './private/CSSModuleThemeContext'; export { default as InjectStyleElements, injectStyleElementsPropsSchema } from './private/InjectStyleElements'; export { default as useStyles } from './private/useStyles'; diff --git a/packages/styles/src/react/private/CSSModuleThemeComposer.spec.tsx b/packages/styles/src/react/private/CSSModuleThemeComposer.spec.tsx new file mode 100644 index 0000000000..55a6cb8e41 --- /dev/null +++ b/packages/styles/src/react/private/CSSModuleThemeComposer.spec.tsx @@ -0,0 +1,71 @@ +/** @jest-environment @happy-dom/jest-environment */ +///