diff --git a/__fixtures__/test-project/web/tsconfig.json b/__fixtures__/test-project/web/tsconfig.json
index e03af54dfc98..b6b53c03d1f4 100644
--- a/__fixtures__/test-project/web/tsconfig.json
+++ b/__fixtures__/test-project/web/tsconfig.json
@@ -31,6 +31,7 @@
},
"include": [
"src",
+ "config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
diff --git a/docs/docs/router.md b/docs/docs/router.md
index 08bbbfdd6d8f..ff4e0ee76d78 100644
--- a/docs/docs/router.md
+++ b/docs/docs/router.md
@@ -128,31 +128,18 @@ becomes...
```
-### `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"
-
-
-
-
-
-
-```
-
-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 `` among your other Sets. So we also provide a `` component that's just an alias for ``. Most of our documentation uses ``.
-
-Here's the same example again, but now using ``
-
```jsx title="Routes.js"
-
+
```
@@ -164,7 +151,7 @@ To protect `Private` routes for access by a single role:
-
+
@@ -176,7 +163,7 @@ To protect `Private` routes for access by multiple roles:
-
+
@@ -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 ``, `` or the `` component:
+In order to display a loader while auth details are being retrieved you can add the `whileLoadingAuth` prop to your private `` or `` component:
```jsx
//Routes.js
diff --git a/packages/cli/src/commands/setup/i18n/i18nHandler.js b/packages/cli/src/commands/setup/i18n/i18nHandler.js
index 362f49749b71..0f87b015d16a 100644
--- a/packages/cli/src/commands/setup/i18n/i18nHandler.js
+++ b/packages/cli/src/commands/setup/i18n/i18nHandler.js
@@ -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')
),
},
{
diff --git a/packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template b/packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template
similarity index 75%
rename from packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template
rename to packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template
index 512bcf7b67b3..a481f82c015a 100644
--- a/packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template
+++ b/packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template
@@ -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',
@@ -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)
diff --git a/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js b/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js
index e088883d4177..b34ea1a3fb57 100644
--- a/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js
+++ b/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js
@@ -108,7 +108,7 @@ export async function handler({ force, install }) {
__dirname,
'..',
'templates',
- 'chakra.storybook.preview.js.template'
+ 'chakra.storybook.preview.tsx.template'
)
),
},
diff --git a/packages/cli/src/commands/setup/ui/libraries/mantine.js b/packages/cli/src/commands/setup/ui/libraries/mantine.js
index d63158285f0b..c77466d9bd66 100644
--- a/packages/cli/src/commands/setup/ui/libraries/mantine.js
+++ b/packages/cli/src/commands/setup/ui/libraries/mantine.js
@@ -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'
)
),
},
diff --git a/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template b/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template
similarity index 58%
rename from packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template
rename to packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template
index b6afe7aa1769..627ffd2f02fc 100644
--- a/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template
+++ b/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template
@@ -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 (
diff --git a/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template b/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template
similarity index 65%
rename from packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template
rename to packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template
index 1e235f622ca0..08c50d47856e 100644
--- a/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template
+++ b/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template
@@ -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 (
diff --git a/packages/cli/src/lib/__tests__/mergeBasics.test.js b/packages/cli/src/lib/__tests__/mergeBasics.test.js
index 28845dbc5aef..cac630c55e73 100644
--- a/packages/cli/src/lib/__tests__/mergeBasics.test.js
+++ b/packages/cli/src/lib/__tests__/mergeBasics.test.js
@@ -35,6 +35,36 @@ describe('the basics', () => {
{ ArrayExpression: concatUnique }
)
})
+ it('Merges JSX strings', () => {
+ const componentA = 'const ComponentA = (props) => Hello
'
+ const componentB = 'const ComponentB = (props) => Bye
'
+ expectTrivialConcat(componentA, componentB)
+ })
+ it('Merges TSX strings', () => {
+ const componentA =
+ 'const ComponentA: MyComponent = (props) => Hello
'
+ const componentB =
+ 'const ComponentB: MyComponent = (props) => Bye
'
+ 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', () => {
diff --git a/packages/cli/src/lib/configureStorybook.js b/packages/cli/src/lib/configureStorybook.js
index cccb7ba18276..3f79e0abb238 100644
--- a/packages/cli/src/lib/configureStorybook.js
+++ b/packages/cli/src/lib/configureStorybook.js
@@ -1,5 +1,4 @@
import path from 'path'
-import util from 'util'
import fse from 'fs-extra'
import prettier from 'prettier'
@@ -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,
@@ -41,6 +62,6 @@ export default async function extendStorybookConfiguration(
...(await prettier.resolveConfig(sbPreviewConfigPath)),
})
- write(sbPreviewConfigPath, formatted)
+ writeFile(sbPreviewConfigPath, formatted, { overwriteExisting: true })
}
}
diff --git a/packages/cli/src/lib/merge/index.js b/packages/cli/src/lib/merge/index.js
index cee121f97d34..bc0637484540 100644
--- a/packages/cli/src/lib/merge/index.js
+++ b/packages/cli/src/lib/merge/index.js
@@ -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'],
})
}
diff --git a/packages/cli/src/lib/templates/storybook.preview.js.template b/packages/cli/src/lib/templates/storybook.preview.js.template
deleted file mode 100644
index 8241d88fab17..000000000000
--- a/packages/cli/src/lib/templates/storybook.preview.js.template
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as React from 'react'
-
-/** @type { import("@storybook/csf").GlobalTypes } */
-export const globalTypes = {}
-
-/**
- * An example, no-op storybook decorator. Use a function like this to create decorators.
- * @param { import("@storybook/addons").StoryFn} StoryFn
- * @param { import("@storybook/addons").StoryContext} context
- * @returns StoryFn, unmodified.
- */
-const _exampleDecorator = (StoryFn, _context) => {
- return
-}
-
-export const decorators = []
diff --git a/packages/cli/src/lib/templates/storybook.preview.tsx.template b/packages/cli/src/lib/templates/storybook.preview.tsx.template
new file mode 100644
index 000000000000..042497ff6196
--- /dev/null
+++ b/packages/cli/src/lib/templates/storybook.preview.tsx.template
@@ -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
+}
+
+export const decorators = []
diff --git a/packages/create-redwood-app/templates/js/web/jsconfig.json b/packages/create-redwood-app/templates/js/web/jsconfig.json
index 3d285cc23dcc..7ddf4c33675e 100644
--- a/packages/create-redwood-app/templates/js/web/jsconfig.json
+++ b/packages/create-redwood-app/templates/js/web/jsconfig.json
@@ -43,6 +43,7 @@
},
"include": [
"src",
+ "config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
diff --git a/packages/create-redwood-app/templates/ts/web/tsconfig.json b/packages/create-redwood-app/templates/ts/web/tsconfig.json
index e03af54dfc98..b6b53c03d1f4 100644
--- a/packages/create-redwood-app/templates/ts/web/tsconfig.json
+++ b/packages/create-redwood-app/templates/ts/web/tsconfig.json
@@ -31,6 +31,7 @@
},
"include": [
"src",
+ "config",
"../.redwood/types/includes/all-*",
"../.redwood/types/includes/web-*",
"../types",
diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts
index cad05c0c4780..a0245b0d1fbb 100644
--- a/packages/project-config/src/__tests__/paths.test.ts
+++ b/packages/project-config/src/__tests__/paths.test.ts
@@ -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',
@@ -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',
@@ -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',
@@ -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',
diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts
index 68c07ddee400..ed79fb75db34 100644
--- a/packages/project-config/src/paths.ts
+++ b/packages/project-config/src/paths.ts
@@ -45,7 +45,7 @@ export interface WebPaths {
entries: string | null
postcss: string
storybookConfig: string
- storybookPreviewConfig: string
+ storybookPreviewConfig: string | null
storybookManagerConfig: string
dist: string
distServer: string
@@ -119,9 +119,8 @@ const PATH_WEB_DIR_GRAPHQL = 'web/src/graphql' // .js,.ts
const PATH_WEB_DIR_CONFIG_POSTCSS = 'web/config/postcss.config.js'
const PATH_WEB_DIR_CONFIG_STORYBOOK_CONFIG = 'web/config/storybook.config.js'
-const PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW = 'web/config/storybook.preview.js'
+const PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW = 'web/config/storybook.preview' // .js, .tsx
const PATH_WEB_DIR_CONFIG_STORYBOOK_MANAGER = 'web/config/storybook.manager.js'
-
const PATH_WEB_DIR_DIST = 'web/dist'
const PATH_WEB_DIR_DIST_SERVER = 'web/dist/server'
const PATH_WEB_DIR_DIST_SERVER_ENTRY_SERVER = 'web/dist/server/entry.server.js'
@@ -229,9 +228,8 @@ export const getPaths = (BASE_DIR: string = getBaseDir()): Paths => {
BASE_DIR,
PATH_WEB_DIR_CONFIG_STORYBOOK_CONFIG
),
- storybookPreviewConfig: path.join(
- BASE_DIR,
- PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW
+ storybookPreviewConfig: resolveFile(
+ path.join(BASE_DIR, PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW)
),
storybookManagerConfig: path.join(
BASE_DIR,
diff --git a/packages/studio/README.md b/packages/studio/README.md
index 86e035ef9645..00fc5207b1d9 100644
--- a/packages/studio/README.md
+++ b/packages/studio/README.md
@@ -161,6 +161,25 @@ Some ideas to improve the Studio are:
* Prisma version
* RedwoodJS Version
+## Troubleshooting
+If you have problems relating to the `@swc` packages then please try adding the following configuration to your `.yarnrc.yml`
+
+```yml
+supportedArchitectures:
+ os:
+ - darwin
+ - linux
+ - win32
+ cpu:
+ - arm64
+ - arm
+ - x64
+ - ia32
+ libc:
+ - glibc
+ - musl
+```
+
## Contributing
We welcome your [feedback](https://community.redwoodjs.com/t/redwood-studio-experimental/4771) and also your contributions to improve Studio.
diff --git a/packages/studio/api/mail/index.ts b/packages/studio/api/mail/index.ts
index 021c6bb03b1a..12674f42540b 100644
--- a/packages/studio/api/mail/index.ts
+++ b/packages/studio/api/mail/index.ts
@@ -1,5 +1,6 @@
import path from 'node:path'
+import * as swc from '@swc/core'
import chokidar from 'chokidar'
import fs from 'fs-extra'
import { simpleParser as simpleMailParser } from 'mailparser'
@@ -10,8 +11,6 @@ import { getPaths } from '@redwoodjs/project-config'
import { getDatabase } from '../database'
import { getStudioConfig } from '../lib/config'
-const swc = require('@swc/core')
-
let smtpServer: SMTPServer
async function insertMailIntoDatabase(mail: any, envelope: any) {
@@ -128,7 +127,7 @@ export async function updateMailTemplates() {
(file) => {
const correspondingDistEntry =
file
- .replace('api/src', 'api/dist')
+ .replace(path.join('api', 'src'), path.join('api', 'dist'))
.substring(0, file.lastIndexOf('.') + 1) + '.js'
return distFiles.includes(correspondingDistEntry)
}
@@ -221,48 +220,220 @@ export async function updateMailTemplates() {
)
}
+function generatePropsTemplate(param: swc.Param | swc.Pattern | null) {
+ // No param means no props template
+ if (!param) {
+ return null
+ }
+
+ // Get the pattern
+ const pattern = param.type === 'Parameter' ? param.pat : param
+ if (!pattern) {
+ return null
+ }
+
+ // Attempt to generate a props template from the pattern
+ let propsTemplate = 'Provide your props here as JSON'
+ try {
+ switch (pattern.type) {
+ case 'Identifier':
+ propsTemplate = `{${pattern.value}: ?}`
+ break
+ case 'AssignmentPattern':
+ if (pattern.left.type === 'ObjectPattern') {
+ propsTemplate = `{${pattern.left.properties
+ .map((p: any) => {
+ return `\n "${p.key.value}": ?`
+ })
+ .join(',')}\n}`
+ }
+ break
+ case 'ObjectPattern':
+ propsTemplate = `{${pattern.properties
+ .map((p: any) => {
+ return `\n "${p.key.value}": ?`
+ })
+ .join(',')}\n}`
+ break
+ }
+ } catch (_error) {
+ // ignore for now, we'll fallback to the generic props template
+ }
+
+ // Fallback to a generic props template if we can't figure out anything more helpful
+ return propsTemplate
+}
+
+function extractNameAndPropsTemplate(
+ component: swc.ModuleItem,
+ functionsAndVariables: swc.ModuleItem[]
+): {
+ name: string
+ propsTemplate: string | null
+} {
+ switch (component.type) {
+ case 'ExportDeclaration':
+ // Arrow functions
+ if (component.declaration.type === 'VariableDeclaration') {
+ // We only support the identifier type for now
+ const identifier = component.declaration.declarations[0].id
+ if (identifier.type !== 'Identifier') {
+ throw new Error('Unexpected identifier type: ' + identifier.type)
+ }
+ // We only support arrow and normal functions for now
+ const expression = component.declaration.declarations[0].init
+ if (!expression) {
+ throw new Error('Unexpected undefined expression')
+ }
+ if (
+ expression.type !== 'ArrowFunctionExpression' &&
+ expression.type !== 'FunctionExpression'
+ ) {
+ throw new Error('Unexpected expression type: ' + expression.type)
+ }
+ return {
+ name: identifier.value,
+ propsTemplate: generatePropsTemplate(expression.params[0] ?? null),
+ }
+ }
+
+ // Normal functions
+ if (component.declaration.type === 'FunctionDeclaration') {
+ return {
+ name: component.declaration.identifier.value,
+ propsTemplate: generatePropsTemplate(
+ component.declaration.params[0] ?? null
+ ),
+ }
+ }
+
+ // Throw for anything else
+ throw new Error(
+ 'Unexpected declaration type: ' + component.declaration.type
+ )
+
+ case 'ExportDefaultExpression':
+ // Arrow functions
+ if (component.expression.type === 'ArrowFunctionExpression') {
+ return {
+ name: 'default',
+ propsTemplate: generatePropsTemplate(
+ component.expression.params[0] ?? null
+ ),
+ }
+ }
+
+ // Variables defined elsewhere and then exported as default
+ if (component.expression.type === 'Identifier') {
+ const expression = component.expression
+ const variable = functionsAndVariables.find((v) => {
+ return (
+ (v.type === 'FunctionDeclaration' &&
+ v.identifier.value === expression.value) || // function
+ (v.type === 'VariableDeclaration' &&
+ v.declarations[0].type === 'VariableDeclarator' &&
+ v.declarations[0].id.type === 'Identifier' &&
+ v.declarations[0].id.value === expression.value) // variable
+ )
+ })
+ if (variable) {
+ if (variable.type === 'FunctionDeclaration') {
+ return {
+ name: variable.identifier.value + ' (default)',
+ propsTemplate: generatePropsTemplate(variable.params[0] ?? null),
+ }
+ }
+ if (variable.type === 'VariableDeclaration') {
+ if (variable.declarations[0].id.type !== 'Identifier') {
+ throw new Error(
+ 'Unexpected identifier type: ' +
+ variable.declarations[0].id.type
+ )
+ }
+ if (
+ variable.declarations[0].init?.type !== 'FunctionExpression' &&
+ variable.declarations[0].init?.type !== 'ArrowFunctionExpression'
+ ) {
+ throw new Error(
+ 'Unexpected init type: ' + variable.declarations[0].init?.type
+ )
+ }
+ return {
+ name: variable.declarations[0].id.value + ' (default)',
+ propsTemplate: generatePropsTemplate(
+ variable.declarations[0].init?.params[0] ?? null
+ ),
+ }
+ }
+ }
+ }
+
+ // Throw for anything else
+ throw new Error(
+ 'Unexpected expression type: ' + component.expression.type
+ )
+
+ case 'ExportDefaultDeclaration':
+ // Normal functions
+ if (component.decl.type === 'FunctionExpression') {
+ let name = 'default'
+ if (component.decl.identifier) {
+ name = component.decl.identifier.value
+ }
+ return {
+ name,
+ propsTemplate: generatePropsTemplate(
+ component.decl.params[0] ?? null
+ ),
+ }
+ }
+
+ // Throw for anything else
+ throw new Error('Unexpected declaration type: ' + component.decl.type)
+
+ default:
+ throw new Error('Unexpected component type: ' + component.type)
+ }
+}
+
function getMailTemplateComponents(templateFilePath: string) {
const ast = swc.parseFileSync(templateFilePath, {
syntax: templateFilePath.endsWith('.js') ? 'ecmascript' : 'typescript',
tsx: templateFilePath.endsWith('.tsx') || templateFilePath.endsWith('.jsx'),
})
- const components = []
+ const components: { name: string; propsTemplate: string | null }[] = []
+ const functionsAndVariables = ast.body.filter((node: any) => {
+ return (
+ node.type === 'VariableDeclaration' || node.type === 'FunctionDeclaration'
+ )
+ })
- // `export function X(){};`
const exportedComponents = ast.body.filter((node: any) => {
- return node.type === 'ExportDeclaration'
+ return [
+ 'ExportDeclaration',
+ 'ExportDefaultDeclaration',
+ 'ExportDefaultExpression',
+ ].includes(node.type)
})
for (let i = 0; i < exportedComponents.length; i++) {
- let propsTemplate = null
- const hasParams = exportedComponents[i].declaration.params.length > 0
- if (hasParams) {
- propsTemplate = 'Provide your props here as JSON'
- try {
- const param = exportedComponents[i].declaration.params[0]
- switch (param.pat.type) {
- case 'ObjectPattern':
- propsTemplate = `{${param.pat.properties
- .map((p: any) => {
- return `\n "${p.key.value}": ?`
- })
- .join(',')}\n}`
- break
- }
- } catch (_error) {
- // Ignore for now
- }
+ try {
+ const { propsTemplate, name } = extractNameAndPropsTemplate(
+ exportedComponents[i],
+ functionsAndVariables
+ )
+ components.push({
+ name,
+ propsTemplate,
+ })
+ } catch (error) {
+ console.error(
+ `Error extracting template component name and props template from ${templateFilePath}:`
+ )
+ console.error(error)
}
- components.push({
- name: exportedComponents[i].declaration?.identifier?.value ?? 'Unknown',
- propsTemplate,
- })
}
- // TODO: Support `const X = () => {}; export default X;`
- // TODO: Support `export default function X () => {}`
- // TODO: Support `export default () => {}`
-
return components
}
@@ -278,7 +449,7 @@ export async function updateMailRenderers() {
const suffix = `studio_${Date.now()}`
const importPath = mailerFilePath.replace('.js', `.${suffix}.js`)
fs.copyFileSync(mailerFilePath, importPath)
- const mailer = (await import(importPath)).mailer
+ const mailer = (await import(`file://${importPath}`)).mailer
fs.removeSync(importPath)
const renderers = Object.keys(mailer.renderers)
const defaultRenderer = mailer.config.rendering.default
diff --git a/packages/studio/api/migrations.ts b/packages/studio/api/migrations.ts
index 4734907ef132..32c25f01785b 100644
--- a/packages/studio/api/migrations.ts
+++ b/packages/studio/api/migrations.ts
@@ -59,9 +59,10 @@ async function migrate001(db: Database) {
CREATE TABLE IF NOT EXISTS mail_template_component (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mail_template_id INTEGER NOT NULL,
- name TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
props_template TEXT,
- updated_at INTEGER DEFAULT (strftime('%s', 'now'))
+ updated_at INTEGER DEFAULT (strftime('%s', 'now')),
+ UNIQUE(mail_template_id, name)
);
CREATE TABLE IF NOT EXISTS mail_renderer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
diff --git a/packages/studio/api/services/mail.ts b/packages/studio/api/services/mail.ts
index 11d107d08b83..da85b81702ff 100644
--- a/packages/studio/api/services/mail.ts
+++ b/packages/studio/api/services/mail.ts
@@ -97,7 +97,7 @@ export async function getRenderedMail(
// Import the template component
const templateComponentDistPath =
template.path
- .replace('api/src', 'api/dist')
+ .replace(path.join('api', 'src'), path.join('api', 'dist'))
.substring(0, template.path.lastIndexOf('.') + 1) + '.js'
const templateImportPath = templateComponentDistPath.replace(
@@ -105,9 +105,14 @@ export async function getRenderedMail(
`.studio_${Date.now()}.js`
)
fs.copyFileSync(templateComponentDistPath, templateImportPath)
- const templateComponent = await import(templateImportPath)
+ const templateComponent = (await import(`file://${templateImportPath}`))
+ .default
fs.removeSync(templateImportPath)
- const Component = templateComponent[component.name]
+
+ const Component =
+ component.name.indexOf('default') !== -1
+ ? templateComponent.default
+ : templateComponent[component.name]
// Import the mailer
const mailerFilePath = path.join(getPaths().api.dist, 'lib', 'mailer.js')
@@ -116,7 +121,7 @@ export async function getRenderedMail(
`.studio_${Date.now()}.js`
)
fs.copyFileSync(mailerFilePath, mailerImportPath)
- const mailer = (await import(mailerImportPath)).mailer
+ const mailer = (await import(`file://${mailerImportPath}`)).mailer
fs.removeSync(mailerImportPath)
// Render the component
diff --git a/packages/studio/package.json b/packages/studio/package.json
index 7e2cfebbb189..e106ae7de79f 100644
--- a/packages/studio/package.json
+++ b/packages/studio/package.json
@@ -41,7 +41,7 @@
"graphql-yoga": "4.0.4",
"jsonwebtoken": "9.0.2",
"lodash": "4.17.21",
- "mailparser": "^3.6.5",
+ "mailparser": "3.6.5",
"pretty-bytes": "5.6.0",
"qs": "6.11.2",
"smtp-server": "3.13.0",
@@ -68,12 +68,12 @@
"@types/aws-lambda": "8.10.126",
"@types/jsonwebtoken": "9.0.5",
"@types/lodash": "4.14.201",
- "@types/mailparser": "^3",
+ "@types/mailparser": "3",
"@types/qs": "6.9.10",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/react-grid-layout": "1",
- "@types/smtp-server": "^3",
+ "@types/smtp-server": "3",
"@types/split2": "4.2.3",
"@types/uuid": "9.0.7",
"@types/yargs": "17.0.31",
@@ -98,5 +98,17 @@
"use-url-search-params": "2.5.1",
"vite": "4.5.1"
},
- "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
+ "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1",
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.3.60",
+ "@swc/core-darwin-x64": "1.3.60",
+ "@swc/core-linux-arm-gnueabihf": "1.3.60",
+ "@swc/core-linux-arm64-gnu": "1.3.60",
+ "@swc/core-linux-arm64-musl": "1.3.60",
+ "@swc/core-linux-x64-gnu": "1.3.60",
+ "@swc/core-linux-x64-musl": "1.3.60",
+ "@swc/core-win32-arm64-msvc": "1.3.60",
+ "@swc/core-win32-ia32-msvc": "1.3.60",
+ "@swc/core-win32-x64-msvc": "1.3.60"
+ }
}
diff --git a/packages/testing/config/storybook/main.js b/packages/testing/config/storybook/main.js
index 2a098541103b..9319ea4d5b9f 100644
--- a/packages/testing/config/storybook/main.js
+++ b/packages/testing/config/storybook/main.js
@@ -74,11 +74,12 @@ const baseConfig = {
}
}
- const userPreviewPath = fs.existsSync(
- redwoodProjectPaths.web.storybookPreviewConfig
- )
- ? redwoodProjectPaths.web.storybookPreviewConfig
- : './preview.example.js'
+ let userPreviewPath = './preview.example.js'
+
+ if (redwoodProjectPaths.storybookPreviewConfig) {
+ userPreviewPath = redwoodProjectPaths.storybookPreviewConfig
+ }
+
sbConfig.resolve.alias['~__REDWOOD__USER_STORYBOOK_PREVIEW_CONFIG'] =
userPreviewPath
diff --git a/yarn.lock b/yarn.lock
index 56aa6c51bc50..9a90e154c7af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9349,17 +9349,27 @@ __metadata:
"@redwoodjs/project-config": "npm:6.0.7"
"@swc/cli": "npm:0.1.62"
"@swc/core": "npm:1.3.60"
+ "@swc/core-darwin-arm64": "npm:1.3.60"
+ "@swc/core-darwin-x64": "npm:1.3.60"
+ "@swc/core-linux-arm-gnueabihf": "npm:1.3.60"
+ "@swc/core-linux-arm64-gnu": "npm:1.3.60"
+ "@swc/core-linux-arm64-musl": "npm:1.3.60"
+ "@swc/core-linux-x64-gnu": "npm:1.3.60"
+ "@swc/core-linux-x64-musl": "npm:1.3.60"
+ "@swc/core-win32-arm64-msvc": "npm:1.3.60"
+ "@swc/core-win32-ia32-msvc": "npm:1.3.60"
+ "@swc/core-win32-x64-msvc": "npm:1.3.60"
"@tailwindcss/forms": "npm:0.5.3"
"@tremor/react": "npm:3.4.1"
"@types/aws-lambda": "npm:8.10.126"
"@types/jsonwebtoken": "npm:9.0.5"
"@types/lodash": "npm:4.14.201"
- "@types/mailparser": "npm:^3"
+ "@types/mailparser": "npm:3"
"@types/qs": "npm:6.9.10"
"@types/react": "npm:18.2.37"
"@types/react-dom": "npm:18.2.15"
"@types/react-grid-layout": "npm:1"
- "@types/smtp-server": "npm:^3"
+ "@types/smtp-server": "npm:3"
"@types/split2": "npm:4.2.3"
"@types/uuid": "npm:9.0.7"
"@types/yargs": "npm:17.0.31"
@@ -9382,7 +9392,7 @@ __metadata:
json-bigint-patch: "npm:0.0.8"
jsonwebtoken: "npm:9.0.2"
lodash: "npm:4.17.21"
- mailparser: "npm:^3.6.5"
+ mailparser: "npm:3.6.5"
postcss: "npm:8.4.31"
pretty-bytes: "npm:5.6.0"
pretty-ms: "npm:7.0.1"
@@ -9404,6 +9414,27 @@ __metadata:
uuid: "npm:9.0.1"
vite: "npm:4.5.1"
yargs: "npm:17.7.2"
+ dependenciesMeta:
+ "@swc/core-darwin-arm64":
+ optional: true
+ "@swc/core-darwin-x64":
+ optional: true
+ "@swc/core-linux-arm-gnueabihf":
+ optional: true
+ "@swc/core-linux-arm64-gnu":
+ optional: true
+ "@swc/core-linux-arm64-musl":
+ optional: true
+ "@swc/core-linux-x64-gnu":
+ optional: true
+ "@swc/core-linux-x64-musl":
+ optional: true
+ "@swc/core-win32-arm64-msvc":
+ optional: true
+ "@swc/core-win32-ia32-msvc":
+ optional: true
+ "@swc/core-win32-x64-msvc":
+ optional: true
languageName: unknown
linkType: soft
@@ -12054,13 +12085,13 @@ __metadata:
languageName: node
linkType: hard
-"@types/mailparser@npm:^3":
- version: 3.4.0
- resolution: "@types/mailparser@npm:3.4.0"
+"@types/mailparser@npm:3":
+ version: 3.4.4
+ resolution: "@types/mailparser@npm:3.4.4"
dependencies:
"@types/node": "npm:*"
iconv-lite: "npm:^0.6.3"
- checksum: 6418bb8414d22c26dcdb3881dbe7bf0bcd100027a818636b06b82c7e70154305132d9b3cbd3f2b30975f58456b5964bd4c68c63fab9bc6808fe73937cc620af3
+ checksum: 5d16e87cebff438f9e725ebb4f4cea4e6c55dfa1d5cdda3c56f3f91b915a0801a84675fee2a8d20b6de20ca8be79678a4e99fb5956104e2eb3344dfac387691c
languageName: node
linkType: hard
@@ -12466,13 +12497,13 @@ __metadata:
languageName: node
linkType: hard
-"@types/smtp-server@npm:^3":
- version: 3.5.7
- resolution: "@types/smtp-server@npm:3.5.7"
+"@types/smtp-server@npm:3":
+ version: 3.5.10
+ resolution: "@types/smtp-server@npm:3.5.10"
dependencies:
"@types/node": "npm:*"
"@types/nodemailer": "npm:*"
- checksum: 183b95fb55946b462034b883b8343529def4eb90fd7ff75ac60769ab84e53e1004cecc867c837c7f9ce1aa46dd0fce8563bf1dbcddd8a5b16b62ec638115e306
+ checksum: 6aa530635c1801470f1eaa4619604ad0590a5fe150dbf7b157d9b49ec272753caaadc4c2905631e83f6ab0163fabcd59fa37a273f7b2458ae7d21ace52914f04
languageName: node
linkType: hard
@@ -25688,7 +25719,7 @@ __metadata:
languageName: node
linkType: hard
-"mailparser@npm:^3.6.5":
+"mailparser@npm:3.6.5":
version: 3.6.5
resolution: "mailparser@npm:3.6.5"
dependencies: