Skip to content

Commit

Permalink
Merge branch 'main' into jgmw-api/context-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe committed Dec 25, 2023
2 parents e322267 + 909ab4e commit efa2f27
Show file tree
Hide file tree
Showing 24 changed files with 420 additions and 149 deletions.
1 change: 1 addition & 0 deletions __fixtures__/test-project/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"include": [
"src",
"config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
Expand Down
25 changes: 6 additions & 19 deletions docs/docs/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,31 +128,18 @@ becomes...
</MainLayout>
```

### `private` Set
### `PrivateSet`

Sets can take a `private` prop which makes all Routes inside that Set require authentication. When a user isn't authenticated and attempts to visit one of the Routes in the private Set, they'll be redirected to the Route passed as the Set's `unauthenticated` prop. The originally-requested Route's path is added to the query string as a `redirectTo` param. This lets you send the user to the page they originally requested once they're logged-in.
A `PrivateSet` makes all Routes inside that Set require authentication. When a user isn't authenticated and attempts to visit one of the Routes in the `PrivateSet`, they'll be redirected to the Route passed as the `PrivateSet`'s `unauthenticated` prop. The originally-requested Route's path is added to the query string as a `redirectTo` param. This lets you send the user to the page they originally requested once they're logged-in.

Here's an example of how you'd use a private set:

```jsx title="Routes.js"
<Router>
<Route path="/" page={HomePage} name="home" />
<Set private unauthenticated="home">
<Route path="/admin" page={AdminPage} name="admin" />
</Set>
</Router>
```

Private routes are important and should be easy to spot in your Routes file. The larger your Routes file gets, the more difficult it will probably become to find `<Set private /*...*/>` among your other Sets. So we also provide a `<PrivateSet>` component that's just an alias for `<Set private /*...*/>`. Most of our documentation uses `<PrivateSet>`.

Here's the same example again, but now using `<PrivateSet>`

```jsx title="Routes.js"
<Router>
<Route path="/" page={HomePage} name="home" />
<PrivateSet unauthenticated="home">
<Route path="/admin" page={AdminPage} name="admin" />
<PrivateSet>
</PrivateSet>
</Router>
```

Expand All @@ -164,7 +151,7 @@ To protect `Private` routes for access by a single role:
<Router>
<PrivateSet unauthenticated="forbidden" roles="admin">
<Route path="/admin/users" page={UsersPage} name="users" />
<PrivateSet>
</PrivateSet>

<Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
</Router>
Expand All @@ -176,7 +163,7 @@ To protect `Private` routes for access by multiple roles:
<Router>
<PrivateSet unauthenticated="forbidden" roles={['admin', 'editor', 'publisher']}>
<Route path="/admin/posts/{id:Int}/edit" page={EditPostPage} name="editPost" />
<PrivateSet>
</PrivateSet>

<Route path="/forbidden" page={ForbiddenPage} name="forbidden" />
</Router>
Expand Down Expand Up @@ -572,7 +559,7 @@ When the lazy-loaded page is loading, `PageLoadingContext.Consumer` will pass `{

Let's say you have a dashboard area on your Redwood app, which can only be accessed after logging in. When Redwood Router renders your private page, it will first fetch the user's details, and only render the page if it determines the user is indeed logged in.

In order to display a loader while auth details are being retrieved you can add the `whileLoadingAuth` prop to your private `<Route>`, `<Set private>` or the `<PrivateSet>` component:
In order to display a loader while auth details are being retrieved you can add the `whileLoadingAuth` prop to your private `<Route>` or `<PrivateSet>` component:

```jsx
//Routes.js
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/setup/i18n/i18nHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const handler = async ({ force }) => {
skip: () => fileIncludes(rwPaths.web.storybookConfig, 'withI18n'),
task: async () =>
extendStorybookConfiguration(
path.join(__dirname, 'templates', 'storybook.preview.js.template')
path.join(__dirname, 'templates', 'storybook.preview.tsx.template')
),
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react'
import { I18nextProvider } from 'react-i18next'
import type { GlobalTypes } from '@storybook/csf'
import type { StoryFn, StoryContext } from '@storybook/react'
import i18n from 'web/src/i18n'

/** @type { import("@storybook/csf").GlobalTypes } */
export const globalTypes = {
export const globalTypes: GlobalTypes = {
locale: {
name: 'Locale',
description: 'Internationalization locale',
Expand All @@ -23,12 +25,10 @@ export const globalTypes = {
* https://github.com/storybookjs/addon-kit/blob/main/src/withGlobals.ts
* Unfortunately that will make eslint complain, so we have to disable it when
* using a hook below
*
* @param { import("@storybook/addons").StoryFn} StoryFn
* @param { import("@storybook/addons").StoryContext} context
* @returns a story wrapped in an I18nextProvider
* @param { import("@storybook/react").StoryFn} StoryFn
* @param { import("@storybook/react").StoryContext} context
*/
const withI18n = (StoryFn, context) => {
const withI18n = (StoryFn: StoryFn, context: StoryContext) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
i18n.changeLanguage(context.globals.locale)
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/setup/ui/libraries/chakra-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export async function handler({ force, install }) {
__dirname,
'..',
'templates',
'chakra.storybook.preview.js.template'
'chakra.storybook.preview.tsx.template'
)
),
},
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/setup/ui/libraries/mantine.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,15 @@ export async function handler({ force, install, packages }) {
},
{
title: 'Configure Storybook...',
skip: () => fileIncludes(rwPaths.web.storybookConfig, 'withMantine'),
skip: () =>
fileIncludes(rwPaths.web.storybookPreviewConfig, 'withMantine'),
task: async () =>
extendStorybookConfiguration(
path.join(
__dirname,
'..',
'templates',
'mantine.storybook.preview.js.template'
'mantine.storybook.preview.tsx.template'
)
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import * as React from 'react'

import { ChakraProvider, extendTheme } from '@chakra-ui/react'
import * as theme from 'config/chakra.config'
import type { StoryFn } from '@storybook/react'
import theme from 'config/chakra.config'

const extendedTheme = extendTheme(theme)

const withChakra = (StoryFn) => {
/**
* @param { import("@storybook/react").StoryFn} StoryFn
*/
const withChakra = (StoryFn: StoryFn) => {
return (
<ChakraProvider theme={extendedTheme}>
<StoryFn />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import * as React from 'react'

import { MantineProvider } from '@mantine/core'
import type { StoryFn } from '@storybook/react'
import theme from 'config/mantine.config'

import '@mantine/core/styles.css'

const withMantine = (StoryFn) => {
/**
* @param { import("@storybook/react").StoryFn} StoryFn
*/
const withMantine = (StoryFn: StoryFn) => {
return (
<MantineProvider theme={theme}>
<StoryFn />
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/lib/__tests__/mergeBasics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ describe('the basics', () => {
{ ArrayExpression: concatUnique }
)
})
it('Merges JSX strings', () => {
const componentA = 'const ComponentA = (props) => <div>Hello</div>'
const componentB = 'const ComponentB = (props) => <div>Bye</div>'
expectTrivialConcat(componentA, componentB)
})
it('Merges TSX strings', () => {
const componentA =
'const ComponentA: MyComponent = (props) => <div>Hello</div>'
const componentB =
'const ComponentB: MyComponent = (props) => <div>Bye</div>'
expectTrivialConcat(componentA, componentB)
})
it('Merges TS strings', () => {
expectMerged(
`\
const x: string = 'x'
const list: string[] = [x]
`,
`\
const y: string = 'y'
const list: string[] = [y]
`,
`\
const x: string = 'x'
const y: string = 'y'
const list: string[] = [x, y]
`,
{ ArrayExpression: concatUnique }
)
})
})

describe('Import behavior', () => {
Expand Down
41 changes: 31 additions & 10 deletions packages/cli/src/lib/configureStorybook.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from 'path'
import util from 'util'

import fse from 'fs-extra'
import prettier from 'prettier'
Expand All @@ -11,24 +10,46 @@ import {
keepBoth,
keepBothStatementParents,
} from './merge/strategy'
import { isTypeScriptProject } from './project'

import { getPaths } from '.'
import { getPaths, transformTSToJS, writeFile } from '.'

/**
* Extends the Storybook configuration file with the new configuration file
* @param {string} newConfigPath - The path to the new configuration file
*/
export default async function extendStorybookConfiguration(
newConfigPath = undefined
) {
const sbPreviewConfigPath = getPaths().web.storybookPreviewConfig
const webPaths = getPaths().web
const ts = isTypeScriptProject()
const sbPreviewConfigPath =
webPaths.storybookPreviewConfig ??
`${webPaths.config}/storybook.preview.${ts ? 'tsx' : 'js'}`
const read = (path) => fse.readFileSync(path, { encoding: 'utf-8' })

if (!fse.existsSync(sbPreviewConfigPath)) {
await util.promisify(fse.cp)(
path.join(__dirname, 'templates', 'storybook.preview.js.template'),
sbPreviewConfigPath
// If the Storybook preview config file doesn't exist, create it from the template
const templateContent = read(
path.resolve(__dirname, 'templates', 'storybook.preview.tsx.template')
)
const storybookPreviewContent = ts
? templateContent
: transformTSToJS(sbPreviewConfigPath, templateContent)

await writeFile(sbPreviewConfigPath, storybookPreviewContent)
}

const storybookPreviewContent = read(sbPreviewConfigPath)

if (newConfigPath) {
const read = (path) => fse.readFileSync(path, { encoding: 'utf-8' })
const write = (path, data) => fse.writeFileSync(path, data)
const merged = merge(read(sbPreviewConfigPath), read(newConfigPath), {
// If the new config file path is provided, merge it with the Storybook preview config file
const newConfigTemplate = read(newConfigPath)
const newConfigContent = ts
? newConfigTemplate
: transformTSToJS(newConfigPath, newConfigTemplate)

const merged = merge(storybookPreviewContent, newConfigContent, {
ImportDeclaration: interleave,
ArrayExpression: concatUnique,
ObjectExpression: concatUnique,
Expand All @@ -41,6 +62,6 @@ export default async function extendStorybookConfiguration(
...(await prettier.resolveConfig(sbPreviewConfigPath)),
})

write(sbPreviewConfigPath, formatted)
writeFile(sbPreviewConfigPath, formatted, { overwriteExisting: true })
}
}
3 changes: 2 additions & 1 deletion packages/cli/src/lib/merge/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ function mergeAST(baseAST, extAST, strategy = {}) {
export function merge(base, extension, strategy) {
function parseReact(code) {
return parse(code, {
presets: ['@babel/preset-react'],
filename: 'merged.tsx', // required to prevent babel error. The .tsx is relevant
presets: ['@babel/preset-typescript'],
})
}

Expand Down
16 changes: 0 additions & 16 deletions packages/cli/src/lib/templates/storybook.preview.js.template

This file was deleted.

18 changes: 18 additions & 0 deletions packages/cli/src/lib/templates/storybook.preview.tsx.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react'

import type { GlobalTypes } from '@storybook/csf'
import type { StoryFn, StoryContext } from '@storybook/react'

/** @type { import("@storybook/csf").GlobalTypes } */
export const globalTypes: GlobalTypes = {}

/**
* An example, no-op storybook decorator. Use a function like this to create decorators.
* @param { import("@storybook/react").StoryFn} StoryFn
* @param { import("@storybook/react").StoryContext} context
*/
const _exampleDecorator = (StoryFn: StoryFn, _context: StoryContext) => {
return <StoryFn />
}

export const decorators = []
1 change: 1 addition & 0 deletions packages/create-redwood-app/templates/js/web/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
},
"include": [
"src",
"config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
Expand Down
1 change: 1 addition & 0 deletions packages/create-redwood-app/templates/ts/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"include": [
"src",
"config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
Expand Down
28 changes: 4 additions & 24 deletions packages/project-config/src/__tests__/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,7 @@ describe('paths', () => {
'config',
'storybook.config.js'
),
storybookPreviewConfig: path.join(
FIXTURE_BASEDIR,
'web',
'config',
'storybook.preview.js'
),
storybookPreviewConfig: null,
storybookManagerConfig: path.join(
FIXTURE_BASEDIR,
'web',
Expand Down Expand Up @@ -411,12 +406,7 @@ describe('paths', () => {
'config',
'storybook.config.js'
),
storybookPreviewConfig: path.join(
FIXTURE_BASEDIR,
'web',
'config',
'storybook.preview.js'
),
storybookPreviewConfig: null,
storybookManagerConfig: path.join(
FIXTURE_BASEDIR,
'web',
Expand Down Expand Up @@ -737,12 +727,7 @@ describe('paths', () => {
'config',
'storybook.config.js'
),
storybookPreviewConfig: path.join(
FIXTURE_BASEDIR,
'web',
'config',
'storybook.preview.js'
),
storybookPreviewConfig: null,
storybookManagerConfig: path.join(
FIXTURE_BASEDIR,
'web',
Expand Down Expand Up @@ -1020,12 +1005,7 @@ describe('paths', () => {
'config',
'storybook.config.js'
),
storybookPreviewConfig: path.join(
FIXTURE_BASEDIR,
'web',
'config',
'storybook.preview.js'
),
storybookPreviewConfig: null,
storybookManagerConfig: path.join(
FIXTURE_BASEDIR,
'web',
Expand Down
Loading

0 comments on commit efa2f27

Please sign in to comment.