From a7c6d5aadb23f655aeaa4ae807ab3ff91150af0f Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Tue, 30 May 2023 12:12:39 -0400 Subject: [PATCH] fix(nextjs): adjust generated CSS-in-JS to match latest Next.js recommendations (#17294) --- e2e/next/src/next-appdir.test.ts | 7 - e2e/next/src/next-styles.test.ts | 13 + .../application/application.spec.ts | 404 ++++++++++++------ .../layout.tsx__tmpl__ | 0 .../app-styled-components/layout.tsx__tmpl__ | 21 + .../registry.tsx__tmpl__ | 33 ++ .../files/app-styled-jsx/layout.tsx__tmpl__ | 16 + .../files/app-styled-jsx/registry.tsx__tmpl__ | 23 + .../files/common/next.config.js__tmpl__ | 11 + .../lib/create-application-files.ts | 25 ++ .../application/lib/show-possible-warnings.ts | 2 +- 11 files changed, 420 insertions(+), 135 deletions(-) rename packages/next/src/generators/application/files/{app => app-default-layout}/layout.tsx__tmpl__ (100%) create mode 100644 packages/next/src/generators/application/files/app-styled-components/layout.tsx__tmpl__ create mode 100644 packages/next/src/generators/application/files/app-styled-components/registry.tsx__tmpl__ create mode 100644 packages/next/src/generators/application/files/app-styled-jsx/layout.tsx__tmpl__ create mode 100644 packages/next/src/generators/application/files/app-styled-jsx/registry.tsx__tmpl__ diff --git a/e2e/next/src/next-appdir.test.ts b/e2e/next/src/next-appdir.test.ts index 9a7b47e32732f..bc21eb5f3d641 100644 --- a/e2e/next/src/next-appdir.test.ts +++ b/e2e/next/src/next-appdir.test.ts @@ -1,18 +1,11 @@ import { cleanupProject, - isNotWindows, - killPorts, newProject, runCLI, - runCommandUntil, - tmpProjPath, uniq, updateFile, } from '@nx/e2e/utils'; -import { getData } from 'ajv/dist/compile/validate'; -import { detectPackageManager } from 'nx/src/utils/package-manager'; import { checkApp } from './utils'; -import { p } from 'vitest/dist/types-b7007192'; describe('Next.js App Router', () => { let proj: string; diff --git a/e2e/next/src/next-styles.test.ts b/e2e/next/src/next-styles.test.ts index 6e1002c7fde6f..5ba0700d4e5e9 100644 --- a/e2e/next/src/next-styles.test.ts +++ b/e2e/next/src/next-styles.test.ts @@ -58,6 +58,19 @@ describe('Next.js apps', () => { checkExport: false, }); + const scAppWithAppRouter = uniq('app'); + + runCLI( + `generate @nx/next:app ${scAppWithAppRouter} --no-interactive --style=styled-components --appDir=true` + ); + + await checkApp(scAppWithAppRouter, { + checkUnitTest: false, // No unit tests for app router + checkLint: false, + checkE2E: false, + checkExport: false, + }); + const emotionApp = uniq('app'); runCLI( diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index ad9d253cb746c..923ed2aaeb1aa 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -15,65 +15,110 @@ describe('app', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); - describe('not nested', () => { - it('should update configurations', async () => { - await applicationGenerator(tree, { - name: 'myApp', - style: 'css', - }); + it('should add a .gitkeep file to the public directory', async () => { + await applicationGenerator(tree, { + name: 'myApp', + style: 'css', + }); - expect(readProjectConfiguration(tree, 'my-app').root).toEqual( - 'apps/my-app' - ); - expect(readProjectConfiguration(tree, 'my-app-e2e').root).toEqual( - 'apps/my-app-e2e' - ); + expect(tree.exists('apps/my-app/public/.gitkeep')).toBe(true); + }); + + it('should update tags and implicit dependencies', async () => { + await applicationGenerator(tree, { + name: 'myApp', + style: 'css', + tags: 'one,two', }); - it('should generate an unstyled component page', async () => { - await applicationGenerator(tree, { - name: 'testApp', - style: 'none', - appDir: false, - }); + const projects = Object.fromEntries(getProjects(tree)); - const content = tree.read('apps/test-app/pages/index.tsx').toString(); + expect(projects).toMatchObject({ + 'my-app': { + tags: ['one', 'two'], + }, + 'my-app-e2e': { + tags: [], + implicitDependencies: ['my-app'], + }, + }); + }); - expect(content).not.toContain('import styles from'); - expect(content).not.toContain('const StyledPage'); - expect(content).not.toContain('className={styles.page}'); + it('should extend from root tsconfig.json when no tsconfig.base.json', async () => { + tree.rename('tsconfig.base.json', 'tsconfig.json'); + + await applicationGenerator(tree, { + name: 'myApp', + style: 'css', }); - it('should update tags and implicit dependencies', async () => { + const tsConfig = readJson(tree, 'apps/my-app/tsconfig.json'); + expect(tsConfig.extends).toBe('../../tsconfig.json'); + }); + + describe('App Router', () => { + it('should generate files for app layout', async () => { await applicationGenerator(tree, { - name: 'myApp', + name: 'testApp', style: 'css', - tags: 'one,two', }); - const projects = Object.fromEntries(getProjects(tree)); + const tsConfig = readJson(tree, 'apps/test-app/tsconfig.json'); + expect(tsConfig.include).toEqual([ + '**/*.ts', + '**/*.tsx', + '**/*.js', + '**/*.jsx', + '../../apps/test-app/.next/types/**/*.ts', + '../../dist/apps/test-app/.next/types/**/*.ts', + 'next-env.d.ts', + ]); + expect(tree.exists('apps/test-app/pages/styles.css')).toBeFalsy(); + expect(tree.exists('apps/test-app/app/global.css')).toBeTruthy(); + expect(tree.exists('apps/test-app/app/page.tsx')).toBeTruthy(); + expect(tree.exists('apps/test-app/app/layout.tsx')).toBeTruthy(); + expect(tree.exists('apps/test-app/app/api/hello/route.ts')).toBeTruthy(); + expect(tree.exists('apps/test-app/app/page.module.css')).toBeTruthy(); + expect(tree.exists('apps/test-app/public/favicon.ico')).toBeTruthy(); + }); - expect(projects).toMatchObject({ - 'my-app': { - tags: ['one', 'two'], - }, - 'my-app-e2e': { - tags: [], - implicitDependencies: ['my-app'], - }, + it('should add layout types correctly for standalone apps', async () => { + await applicationGenerator(tree, { + name: 'testApp', + style: 'css', + appDir: true, + rootProject: true, }); + + const tsConfig = readJson(tree, 'tsconfig.json'); + expect(tsConfig.include).toEqual([ + '**/*.ts', + '**/*.tsx', + '**/*.js', + '**/*.jsx', + '.next/types/**/*.ts', + 'dist/test-app/.next/types/**/*.ts', + 'next-env.d.ts', + ]); }); - it('should generate files for app router layout', async () => { + it('should generate an unstyled component page', async () => { await applicationGenerator(tree, { - name: 'myApp', - style: 'css', + name: 'testApp', + style: 'none', + appDir: true, + rootProject: true, }); - expect(tree.exists('apps/my-app/tsconfig.json')).toBeTruthy(); - expect(tree.exists('apps/my-app/app/page.tsx')).toBeTruthy(); - expect(tree.exists('apps/my-app/app/page.module.css')).toBeTruthy(); + + const content = tree.read('app/page.tsx').toString(); + + expect(content).not.toContain('import styles from'); + expect(content).not.toContain('const StyledPage'); + expect(content).not.toContain('className={styles.page}'); }); + }); + describe('Pages Router', () => { it('should generate files for pages layout', async () => { await applicationGenerator(tree, { name: 'myApp', @@ -86,26 +131,32 @@ describe('app', () => { expect(tree.exists('apps/my-app/pages/index.module.css')).toBeTruthy(); }); - it('should extend from root tsconfig.base.json', async () => { + it('should update configurations', async () => { await applicationGenerator(tree, { name: 'myApp', style: 'css', }); - const tsConfig = readJson(tree, 'apps/my-app/tsconfig.json'); - expect(tsConfig.extends).toBe('../../tsconfig.base.json'); + expect(readProjectConfiguration(tree, 'my-app').root).toEqual( + 'apps/my-app' + ); + expect(readProjectConfiguration(tree, 'my-app-e2e').root).toEqual( + 'apps/my-app-e2e' + ); }); - it('should extend from root tsconfig.json when no tsconfig.base.json', async () => { - tree.rename('tsconfig.base.json', 'tsconfig.json'); - + it('should generate an unstyled component page', async () => { await applicationGenerator(tree, { - name: 'myApp', - style: 'css', + name: 'testApp', + style: 'none', + appDir: false, }); - const tsConfig = readJson(tree, 'apps/my-app/tsconfig.json'); - expect(tsConfig.extends).toBe('../../tsconfig.json'); + const content = tree.read('apps/test-app/pages/index.tsx').toString(); + + expect(content).not.toContain('import styles from'); + expect(content).not.toContain('const StyledPage'); + expect(content).not.toContain('className={styles.page}'); }); }); @@ -121,6 +172,28 @@ describe('app', () => { const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8'); expect(indexContent).toContain(`import styles from './page.module.scss'`); + expect(tree.read('apps/my-app/app/layout.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import './global.css'; + + export const metadata = { + title: 'Welcome to my-app', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + } + " + `); }); }); @@ -136,6 +209,28 @@ describe('app', () => { const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8'); expect(indexContent).toContain(`import styles from './page.module.less'`); + expect(tree.read('apps/my-app/app/layout.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import './global.less'; + + export const metadata = { + title: 'Welcome to my-app', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + } + " + `); }); }); @@ -155,7 +250,7 @@ describe('app', () => { }); describe('--style styled-components', () => { - it('should generate scss styles', async () => { + it('should generate styles', async () => { await applicationGenerator(tree, { name: 'myApp', style: 'styled-components', @@ -169,11 +264,73 @@ describe('app', () => { const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from 'styled-components'`); + expect(tree.read('apps/my-app/app/layout.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import './global.css'; + import { StyledComponentsRegistry } from './registry'; + + export const metadata = { + title: 'Welcome to demo2', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + + {children} + + + ); + } + " + `); + expect(tree.read('apps/my-app/app/registry.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "'use client'; + + import React, { useState } from 'react'; + import { useServerInsertedHTML } from 'next/navigation'; + import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; + + export function StyledComponentsRegistry({ + children, + }: { + children: React.ReactNode; + }) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); + + useServerInsertedHTML(() => { + const styles = styledComponentsStyleSheet.getStyleElement(); + + // Types are out of date, clearTag is not defined. + // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/65021 + (styledComponentsStyleSheet.instance as any).clearTag(); + + return <>{styles}; + }); + + if (typeof window !== 'undefined') return <>{children}; + + return ( + + {children} + + ); + } + " + `); }); }); describe('--style @emotion/styled', () => { - it('should generate scss styles', async () => { + it('should generate styles', async () => { await applicationGenerator(tree, { name: 'myApp', style: '@emotion/styled', @@ -187,6 +344,28 @@ describe('app', () => { const indexContent = tree.read('apps/my-app/app/page.tsx', 'utf-8'); expect(indexContent).not.toContain(`import styles from './page.module`); expect(indexContent).toContain(`import styled from '@emotion/styled'`); + expect(tree.read('apps/my-app/app/layout.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import './global.css'; + + export const metadata = { + title: 'Welcome to my-app', + description: 'Generated by create-nx-workspace', + }; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + } + " + `); }); it('should add jsxImportSource in tsconfig.json', async () => { @@ -220,6 +399,49 @@ describe('app', () => { expect(indexContent).not.toContain( `import styled from 'styled-components'` ); + expect(tree.read('apps/my-app/app/layout.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "import './global.css'; + import { StyledJsxRegistry } from './registry'; + + export default function RootLayout({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + + {children} + + + ); + } + " + `); + expect(tree.read('apps/my-app/app/registry.tsx', 'utf-8')) + .toMatchInlineSnapshot(` + "'use client'; + + import React, { useState } from 'react'; + import { useServerInsertedHTML } from 'next/navigation'; + import { StyleRegistry, createStyleRegistry } from 'styled-jsx'; + + export function StyledJsxRegistry({ children }: { children: React.ReactNode }) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [jsxStyleRegistry] = useState(() => createStyleRegistry()); + + useServerInsertedHTML(() => { + const styles = jsxStyleRegistry.styles(); + jsxStyleRegistry.flush(); + return <>{styles}; + }); + + return {children}; + } + " + `); }); }); @@ -245,7 +467,7 @@ describe('app', () => { ); }); - it('should set up the nrwl next build builder', async () => { + it('should set up the nx next build builder', async () => { await applicationGenerator(tree, { name: 'my-app', style: 'css', @@ -260,7 +482,7 @@ describe('app', () => { }); }); - it('should set up the nrwl next server builder', async () => { + it('should set up the nx next server builder', async () => { await applicationGenerator(tree, { name: 'my-app', style: 'css', @@ -283,7 +505,7 @@ describe('app', () => { }); }); - it('should set up the nrwl next export builder', async () => { + it('should set up the nx next export builder', async () => { await applicationGenerator(tree, { name: 'my-app', style: 'css', @@ -448,76 +670,4 @@ describe('app', () => { expect(tsConfigApp.exclude).not.toContain('**/*.spec.js'); }); }); - - it('should add a .gitkeep file to the public directory', async () => { - await applicationGenerator(tree, { - name: 'myApp', - style: 'css', - }); - - expect(tree.exists('apps/my-app/public/.gitkeep')).toBe(true); - }); - - describe('--appDir', () => { - it('should generate app directory instead of pages', async () => { - await applicationGenerator(tree, { - name: 'testApp', - style: 'css', - appDir: true, - }); - - const tsConfig = readJson(tree, 'apps/test-app/tsconfig.json'); - expect(tsConfig.include).toEqual([ - '**/*.ts', - '**/*.tsx', - '**/*.js', - '**/*.jsx', - '../../apps/test-app/.next/types/**/*.ts', - '../../dist/apps/test-app/.next/types/**/*.ts', - 'next-env.d.ts', - ]); - expect(tree.exists('apps/test-app/pages/styles.css')).toBeFalsy(); - expect(tree.exists('apps/test-app/app/global.css')).toBeTruthy(); - expect(tree.exists('apps/test-app/app/page.tsx')).toBeTruthy(); - expect(tree.exists('apps/test-app/app/layout.tsx')).toBeTruthy(); - expect(tree.exists('apps/test-app/app/api/hello/route.ts')).toBeTruthy(); - expect(tree.exists('apps/test-app/app/page.module.css')).toBeTruthy(); - expect(tree.exists('apps/test-app/public/favicon.ico')).toBeTruthy(); - }); - - it('should add layout types correctly for standalone apps', async () => { - await applicationGenerator(tree, { - name: 'testApp', - style: 'css', - appDir: true, - rootProject: true, - }); - - const tsConfig = readJson(tree, 'tsconfig.json'); - expect(tsConfig.include).toEqual([ - '**/*.ts', - '**/*.tsx', - '**/*.js', - '**/*.jsx', - '.next/types/**/*.ts', - 'dist/test-app/.next/types/**/*.ts', - 'next-env.d.ts', - ]); - }); - - it('should generate an unstyled component page', async () => { - await applicationGenerator(tree, { - name: 'testApp', - style: 'none', - appDir: true, - rootProject: true, - }); - - const content = tree.read('app/page.tsx').toString(); - - expect(content).not.toContain('import styles from'); - expect(content).not.toContain('const StyledPage'); - expect(content).not.toContain('className={styles.page}'); - }); - }); }); diff --git a/packages/next/src/generators/application/files/app/layout.tsx__tmpl__ b/packages/next/src/generators/application/files/app-default-layout/layout.tsx__tmpl__ similarity index 100% rename from packages/next/src/generators/application/files/app/layout.tsx__tmpl__ rename to packages/next/src/generators/application/files/app-default-layout/layout.tsx__tmpl__ diff --git a/packages/next/src/generators/application/files/app-styled-components/layout.tsx__tmpl__ b/packages/next/src/generators/application/files/app-styled-components/layout.tsx__tmpl__ new file mode 100644 index 0000000000000..38ac98e0d279c --- /dev/null +++ b/packages/next/src/generators/application/files/app-styled-components/layout.tsx__tmpl__ @@ -0,0 +1,21 @@ +import './global.<%= stylesExt %>'; +import { StyledComponentsRegistry } from './registry'; + +export const metadata = { + title: 'Welcome to demo2', + description: 'Generated by create-nx-workspace', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/packages/next/src/generators/application/files/app-styled-components/registry.tsx__tmpl__ b/packages/next/src/generators/application/files/app-styled-components/registry.tsx__tmpl__ new file mode 100644 index 0000000000000..35e10cce275b9 --- /dev/null +++ b/packages/next/src/generators/application/files/app-styled-components/registry.tsx__tmpl__ @@ -0,0 +1,33 @@ +'use client'; + +import React, { useState } from 'react'; +import { useServerInsertedHTML } from 'next/navigation'; +import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; + +export function StyledComponentsRegistry({ + children, +}: { + children: React.ReactNode; +}) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()); + + useServerInsertedHTML(() => { + const styles = styledComponentsStyleSheet.getStyleElement(); + + // Types are out of date, clearTag is not defined. + // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/65021 + (styledComponentsStyleSheet.instance as any).clearTag(); + + return <>{styles}; + }); + + if (typeof window !== 'undefined') return <>{children}; + + return ( + + {children} + + ); +} diff --git a/packages/next/src/generators/application/files/app-styled-jsx/layout.tsx__tmpl__ b/packages/next/src/generators/application/files/app-styled-jsx/layout.tsx__tmpl__ new file mode 100644 index 0000000000000..791aa9f2e71b3 --- /dev/null +++ b/packages/next/src/generators/application/files/app-styled-jsx/layout.tsx__tmpl__ @@ -0,0 +1,16 @@ +import './global.<%= stylesExt %>'; +import { StyledJsxRegistry } from './registry'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/packages/next/src/generators/application/files/app-styled-jsx/registry.tsx__tmpl__ b/packages/next/src/generators/application/files/app-styled-jsx/registry.tsx__tmpl__ new file mode 100644 index 0000000000000..db1ed18cfa252 --- /dev/null +++ b/packages/next/src/generators/application/files/app-styled-jsx/registry.tsx__tmpl__ @@ -0,0 +1,23 @@ +'use client'; + +import React, { useState } from 'react'; +import { useServerInsertedHTML } from 'next/navigation'; +import { StyleRegistry, createStyleRegistry } from 'styled-jsx'; + +export function StyledJsxRegistry({ + children, +}: { + children: React.ReactNode; +}) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [jsxStyleRegistry] = useState(() => createStyleRegistry()); + + useServerInsertedHTML(() => { + const styles = jsxStyleRegistry.styles(); + jsxStyleRegistry.flush(); + return <>{styles}; + }); + + return {children}; +} diff --git a/packages/next/src/generators/application/files/common/next.config.js__tmpl__ b/packages/next/src/generators/application/files/common/next.config.js__tmpl__ index eaecc4924c881..76477fe502d04 100644 --- a/packages/next/src/generators/application/files/common/next.config.js__tmpl__ +++ b/packages/next/src/generators/application/files/common/next.config.js__tmpl__ @@ -62,6 +62,17 @@ const nextConfig = { // See: https://github.com/gregberge/svgr svgr: false, }, + <% if (style === 'styled-components') { %> + compiler: { + // For other options, see https://styled-components.com/docs/tooling#babel-plugin + styledComponents: true, + }, + <% } else if (style === '@emotion/styled') { %> + compiler: { + // For other options, see https://nextjs.org/docs/architecture/nextjs-compiler#emotion + emotion: true, + }, + <% } %> }; const plugins = [ diff --git a/packages/next/src/generators/application/lib/create-application-files.ts b/packages/next/src/generators/application/lib/create-application-files.ts index ba7d46fab7a11..5068cc2e9fc18 100644 --- a/packages/next/src/generators/application/lib/create-application-files.ts +++ b/packages/next/src/generators/application/lib/create-application-files.ts @@ -65,6 +65,8 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { join(options.appProjectRoot, 'app'), templateVariables ); + + // RSC is not possible to unit test without extra helpers for data fetching. Leaving it to the user to figure out. host.delete( joinPathFragments( options.appProjectRoot, @@ -72,6 +74,29 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { `index.spec.${options.js ? 'jsx' : 'tsx'}` ) ); + + if (options.style === 'styled-components') { + generateFiles( + host, + join(__dirname, '../files/app-styled-components'), + join(options.appProjectRoot, 'app'), + templateVariables + ); + } else if (options.style === 'styled-jsx') { + generateFiles( + host, + join(__dirname, '../files/app-styled-jsx'), + join(options.appProjectRoot, 'app'), + templateVariables + ); + } else { + generateFiles( + host, + join(__dirname, '../files/app-default-layout'), + join(options.appProjectRoot, 'app'), + templateVariables + ); + } } else { generateFiles( host, diff --git a/packages/next/src/generators/application/lib/show-possible-warnings.ts b/packages/next/src/generators/application/lib/show-possible-warnings.ts index 39eb119623adc..c73a8e0b3f508 100644 --- a/packages/next/src/generators/application/lib/show-possible-warnings.ts +++ b/packages/next/src/generators/application/lib/show-possible-warnings.ts @@ -4,7 +4,7 @@ import { NormalizedSchema } from './normalize-options'; export function showPossibleWarnings(tree: Tree, options: NormalizedSchema) { if (options.style === '@emotion/styled' && options.appDir) { logger.warn( - `Emotion may not work with the App Router. See: https://beta.nextjs.org/docs/styling/css-in-js` + `Emotion may not work with the App Router. See: https://nextjs.org/docs/app/building-your-application/styling/css-in-js` ); } }