From 300c39d7b5a947c6d48902dc2fffa45ecb4df22b Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 9 Aug 2025 12:26:56 +0200
Subject: [PATCH 1/5] feat(esm): Adding experimental support for ESM apps
---
.github/workflows/ci.yml | 9 +
.../esm-fragment-test-project/.editorconfig | 10 +
.../esm-fragment-test-project/.env.defaults | 19 ++
.../esm-fragment-test-project/.env.example | 4 +
.../esm-fragment-test-project/.gitignore | 24 ++
.../.redwood/README.md | 44 ++++
.../.vscode/extensions.json | 16 ++
.../.vscode/launch.json | 56 ++++
.../.vscode/settings.json | 17 ++
.../.vscode/tasks.json | 29 +++
.../esm-fragment-test-project/.yarnrc.yml | 15 ++
.../esm-fragment-test-project/README.md | 17 ++
.../migration.sql | 34 +++
.../migration.sql | 8 +
.../migration.sql | 28 ++
.../api/db/migrations/migration_lock.toml | 3 +
.../api/db/schema.prisma | 79 ++++++
.../api/package.json | 11 +
.../api/src/__tests__/context.test.ts | 14 +
.../requireAuth/requireAuth.test.ts | 20 ++
.../src/directives/requireAuth/requireAuth.ts | 25 ++
.../src/directives/skipAuth/skipAuth.test.ts | 10 +
.../api/src/directives/skipAuth/skipAuth.ts | 16 ++
.../api/src/functions/auth.ts | 202 +++++++++++++++
.../api/src/functions/graphql.ts | 25 ++
.../api/src/graphql/.keep | 0
.../api/src/graphql/contacts.sdl.ts | 32 +++
.../api/src/graphql/groceries.sdl.ts | 49 ++++
.../api/src/graphql/posts.sdl.ts | 33 +++
.../api/src/graphql/produces.sdl.ts | 54 ++++
.../api/src/graphql/stalls.sdl.ts | 29 +++
.../api/src/graphql/users.sdl.ts | 25 ++
.../api/src/lib/auth.ts | 121 +++++++++
.../api/src/lib/db.ts | 26 ++
.../api/src/lib/logger.ts | 17 ++
.../api/src/services/.keep | 0
.../services/contacts/contacts.scenarios.ts | 12 +
.../src/services/contacts/contacts.test.ts | 59 +++++
.../api/src/services/contacts/contacts.ts | 37 +++
.../contacts/describeContacts.scenarios.ts | 12 +
.../contacts/describeContacts.test.ts | 57 ++++
.../api/src/services/groceries.ts | 32 +++
.../api/src/services/posts/posts.scenarios.ts | 38 +++
.../api/src/services/posts/posts.test.ts | 55 ++++
.../api/src/services/posts/posts.ts | 42 +++
.../services/produces/produces.scenarios.ts | 28 ++
.../src/services/produces/produces.test.ts | 67 +++++
.../api/src/services/produces/produces.ts | 47 ++++
.../src/services/stalls/stalls.scenarios.ts | 12 +
.../api/src/services/stalls/stalls.test.ts | 56 ++++
.../api/src/services/stalls/stalls.ts | 45 ++++
.../api/src/services/users/users.scenarios.ts | 26 ++
.../api/src/services/users/users.test.ts | 10 +
.../api/src/services/users/users.ts | 17 ++
.../api/tsconfig.json | 26 ++
.../api/vitest.config.ts | 10 +
.../graphql.config.cjs | 11 +
.../esm-fragment-test-project/package.json | 32 +++
.../prettier.config.cjs | 20 ++
.../esm-fragment-test-project/redwood.toml | 24 ++
.../esm-fragment-test-project/scripts/.keep | 0
.../esm-fragment-test-project/scripts/seed.ts | 159 ++++++++++++
.../scripts/tsconfig.json | 29 +++
.../vitest.config.ts | 7 +
.../web/config/postcss.config.cjs | 9 +
.../web/config/tailwind.config.cjs | 10 +
.../web/package.json | 32 +++
.../web/public/README.md | 43 ++++
.../web/public/favicon.png | Bin 0 -> 2500 bytes
.../web/public/robots.txt | 2 +
.../esm-fragment-test-project/web/src/App.tsx | 29 +++
.../web/src/Redwood.stories.mdx | 18 ++
.../web/src/Routes.tsx | 54 ++++
.../esm-fragment-test-project/web/src/auth.ts | 5 +
.../web/src/components/.keep | 0
.../src/components/Author/Author.stories.tsx | 35 +++
.../web/src/components/Author/Author.test.tsx | 19 ++
.../web/src/components/Author/Author.tsx | 16 ++
.../components/AuthorCell/AuthorCell.mock.ts | 9 +
.../AuthorCell/AuthorCell.stories.tsx | 35 +++
.../components/AuthorCell/AuthorCell.test.tsx | 42 +++
.../src/components/AuthorCell/AuthorCell.tsx | 42 +++
.../components/BlogPost/BlogPost.stories.tsx | 26 ++
.../src/components/BlogPost/BlogPost.test.tsx | 14 +
.../web/src/components/BlogPost/BlogPost.tsx | 41 +++
.../BlogPostCell/BlogPostCell.mock.ts | 17 ++
.../BlogPostCell/BlogPostCell.stories.tsx | 35 +++
.../BlogPostCell/BlogPostCell.test.tsx | 42 +++
.../components/BlogPostCell/BlogPostCell.tsx | 46 ++++
.../BlogPostsCell/BlogPostsCell.mock.ts | 47 ++++
.../BlogPostsCell/BlogPostsCell.stories.tsx | 35 +++
.../BlogPostsCell/BlogPostsCell.test.tsx | 42 +++
.../BlogPostsCell/BlogPostsCell.tsx | 45 ++++
.../web/src/components/Card.tsx | 9 +
.../ClassWithClassField.stories.tsx | 26 ++
.../ClassWithClassField.test.tsx | 14 +
.../ClassWithClassField.tsx | 12 +
.../components/Contact/Contact/Contact.tsx | 98 +++++++
.../Contact/ContactCell/ContactCell.tsx | 43 ++++
.../Contact/ContactForm/ContactForm.tsx | 101 ++++++++
.../components/Contact/Contacts/Contacts.tsx | 102 ++++++++
.../Contact/ContactsCell/ContactsCell.tsx | 46 ++++
.../EditContactCell/EditContactCell.tsx | 89 +++++++
.../Contact/NewContact/NewContact.tsx | 55 ++++
.../web/src/components/FruitInfo.tsx | 35 +++
.../Post/EditPostCell/EditPostCell.tsx | 78 ++++++
.../src/components/Post/NewPost/NewPost.tsx | 52 ++++
.../web/src/components/Post/Post/Post.tsx | 98 +++++++
.../src/components/Post/PostCell/PostCell.tsx | 36 +++
.../src/components/Post/PostForm/PostForm.tsx | 102 ++++++++
.../web/src/components/Post/Posts/Posts.tsx | 102 ++++++++
.../components/Post/PostsCell/PostsCell.tsx | 45 ++++
.../web/src/components/ProduceInfo.tsx | 26 ++
.../web/src/components/StallInfo.tsx | 24 ++
.../web/src/components/VegetableInfo.tsx | 35 +++
.../WaterfallBlogPostCell.mock.ts | 17 ++
.../WaterfallBlogPostCell.stories.tsx | 35 +++
.../WaterfallBlogPostCell.test.tsx | 44 ++++
.../WaterfallBlogPostCell.tsx | 67 +++++
.../web/src/entry.client.tsx | 35 +++
.../web/src/graphql/possibleTypes.ts | 12 +
.../web/src/index.css | 13 +
.../web/src/index.html | 15 ++
.../web/src/layouts/.keep | 0
.../layouts/BlogLayout/BlogLayout.stories.tsx | 13 +
.../layouts/BlogLayout/BlogLayout.test.tsx | 14 +
.../web/src/layouts/BlogLayout/BlogLayout.tsx | 85 ++++++
.../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 37 +++
.../web/src/lib/formatters.test.tsx | 192 ++++++++++++++
.../web/src/lib/formatters.tsx | 58 +++++
.../src/pages/AboutPage/AboutPage.stories.tsx | 13 +
.../src/pages/AboutPage/AboutPage.test.tsx | 14 +
.../web/src/pages/AboutPage/AboutPage.tsx | 10 +
.../BlogPostPage/BlogPostPage.routeHooks.ts | 5 +
.../BlogPostPage/BlogPostPage.stories.tsx | 17 ++
.../pages/BlogPostPage/BlogPostPage.test.tsx | 14 +
.../src/pages/BlogPostPage/BlogPostPage.tsx | 20 ++
.../pages/Contact/ContactPage/ContactPage.tsx | 11 +
.../Contact/ContactsPage/ContactsPage.tsx | 7 +
.../EditContactPage/EditContactPage.tsx | 11 +
.../Contact/NewContactPage/NewContactPage.tsx | 7 +
.../ContactUsPage/ContactUsPage.stories.tsx | 13 +
.../ContactUsPage/ContactUsPage.test.tsx | 14 +
.../src/pages/ContactUsPage/ContactUsPage.tsx | 133 ++++++++++
.../pages/DoublePage/DoublePage.stories.tsx | 13 +
.../src/pages/DoublePage/DoublePage.test.tsx | 14 +
.../web/src/pages/DoublePage/DoublePage.tsx | 39 +++
.../web/src/pages/DoublePage/test.png | Bin 0 -> 2500 bytes
.../pages/FatalErrorPage/FatalErrorPage.tsx | 57 ++++
.../ForgotPasswordPage/ForgotPasswordPage.tsx | 94 +++++++
.../GroceriesPage/GroceriesPage.stories.tsx | 13 +
.../GroceriesPage/GroceriesPage.test.tsx | 14 +
.../src/pages/GroceriesPage/GroceriesPage.tsx | 55 ++++
.../src/pages/HomePage/HomePage.stories.tsx | 13 +
.../web/src/pages/HomePage/HomePage.test.tsx | 14 +
.../web/src/pages/HomePage/HomePage.tsx | 7 +
.../web/src/pages/LoginPage/LoginPage.tsx | 133 ++++++++++
.../src/pages/NotFoundPage/NotFoundPage.tsx | 44 ++++
.../pages/Post/EditPostPage/EditPostPage.tsx | 11 +
.../pages/Post/NewPostPage/NewPostPage.tsx | 7 +
.../web/src/pages/Post/PostPage/PostPage.tsx | 11 +
.../src/pages/Post/PostsPage/PostsPage.tsx | 7 +
.../pages/ProfilePage/ProfilePage.stories.tsx | 13 +
.../pages/ProfilePage/ProfilePage.test.tsx | 21 ++
.../web/src/pages/ProfilePage/ProfilePage.tsx | 55 ++++
.../ResetPasswordPage/ResetPasswordPage.tsx | 115 +++++++++
.../web/src/pages/SignupPage/SignupPage.tsx | 147 +++++++++++
.../WaterfallPage/WaterfallPage.routeHooks.ts | 3 +
.../WaterfallPage/WaterfallPage.stories.tsx | 17 ++
.../WaterfallPage/WaterfallPage.test.tsx | 14 +
.../src/pages/WaterfallPage/WaterfallPage.tsx | 11 +
.../web/src/scaffold.css | 243 ++++++++++++++++++
.../web/tsconfig.json | 44 ++++
.../web/vite.config.ts | 21 ++
.../web/vitest.setup.ts | 12 +
__fixtures__/esm-test-project/.editorconfig | 10 +
__fixtures__/esm-test-project/.env.defaults | 19 ++
__fixtures__/esm-test-project/.env.example | 4 +
__fixtures__/esm-test-project/.gitignore | 24 ++
.../esm-test-project/.redwood/README.md | 44 ++++
.../esm-test-project/.vscode/extensions.json | 16 ++
.../esm-test-project/.vscode/launch.json | 56 ++++
.../esm-test-project/.vscode/settings.json | 17 ++
.../esm-test-project/.vscode/tasks.json | 29 +++
__fixtures__/esm-test-project/.yarnrc.yml | 15 ++
__fixtures__/esm-test-project/README.md | 17 ++
.../migration.sql | 34 +++
.../migration.sql | 8 +
.../api/db/migrations/migration_lock.toml | 3 +
.../esm-test-project/api/db/schema.prisma | 53 ++++
.../esm-test-project/api/package.json | 11 +
.../api/src/__tests__/1-db-import.test.ts | 5 +
.../api/src/__tests__/2-db-import.test.ts | 10 +
.../api/src/__tests__/3-db-import.test.ts | 5 +
.../api/src/__tests__/context.test.ts | 14 +
.../requireAuth/requireAuth.test.ts | 20 ++
.../src/directives/requireAuth/requireAuth.ts | 25 ++
.../src/directives/skipAuth/skipAuth.test.ts | 10 +
.../api/src/directives/skipAuth/skipAuth.ts | 16 ++
.../api/src/functions/auth.ts | 202 +++++++++++++++
.../api/src/functions/graphql.ts | 25 ++
.../esm-test-project/api/src/graphql/.keep | 0
.../api/src/graphql/contacts.sdl.ts | 32 +++
.../api/src/graphql/posts.sdl.ts | 33 +++
.../api/src/graphql/users.sdl.ts | 25 ++
.../esm-test-project/api/src/lib/auth.ts | 121 +++++++++
.../esm-test-project/api/src/lib/db.ts | 26 ++
.../esm-test-project/api/src/lib/logger.ts | 17 ++
.../esm-test-project/api/src/services/.keep | 0
.../services/contacts/contacts.scenarios.ts | 12 +
.../src/services/contacts/contacts.test.ts | 59 +++++
.../api/src/services/contacts/contacts.ts | 37 +++
.../contacts/describeContacts.scenarios.ts | 12 +
.../contacts/describeContacts.test.ts | 57 ++++
.../api/src/services/posts/posts.scenarios.ts | 38 +++
.../api/src/services/posts/posts.test.ts | 55 ++++
.../api/src/services/posts/posts.ts | 42 +++
.../api/src/services/users/users.scenarios.ts | 26 ++
.../api/src/services/users/users.test.ts | 10 +
.../api/src/services/users/users.ts | 17 ++
.../esm-test-project/api/tsconfig.json | 26 ++
.../api/vitest-sort.config.ts | 26 ++
.../esm-test-project/api/vitest.config.ts | 10 +
.../esm-test-project/graphql.config.cjs | 11 +
__fixtures__/esm-test-project/package.json | 33 +++
.../esm-test-project/prettier.config.cjs | 20 ++
__fixtures__/esm-test-project/redwood.toml | 21 ++
__fixtures__/esm-test-project/scripts/.keep | 0
.../esm-test-project/scripts/i/am/nested.ts | 15 ++
.../scripts/one/two/myNestedScript.ts | 3 +
__fixtures__/esm-test-project/scripts/seed.ts | 88 +++++++
.../esm-test-project/scripts/tsconfig.json | 29 +++
.../esm-test-project/vitest.config.ts | 7 +
.../web/config/postcss.config.cjs | 9 +
.../web/config/tailwind.config.cjs | 10 +
.../esm-test-project/web/package.json | 32 +++
.../esm-test-project/web/public/README.md | 43 ++++
.../esm-test-project/web/public/favicon.png | Bin 0 -> 2500 bytes
.../esm-test-project/web/public/robots.txt | 2 +
__fixtures__/esm-test-project/web/src/App.tsx | 29 +++
.../web/src/Redwood.stories.mdx | 18 ++
.../esm-test-project/web/src/Routes.tsx | 53 ++++
__fixtures__/esm-test-project/web/src/auth.ts | 5 +
.../esm-test-project/web/src/components/.keep | 0
.../src/components/Author/Author.stories.tsx | 35 +++
.../web/src/components/Author/Author.test.tsx | 19 ++
.../web/src/components/Author/Author.tsx | 16 ++
.../components/AuthorCell/AuthorCell.mock.ts | 9 +
.../AuthorCell/AuthorCell.stories.tsx | 35 +++
.../components/AuthorCell/AuthorCell.test.tsx | 42 +++
.../src/components/AuthorCell/AuthorCell.tsx | 42 +++
.../components/BlogPost/BlogPost.stories.tsx | 26 ++
.../src/components/BlogPost/BlogPost.test.tsx | 14 +
.../web/src/components/BlogPost/BlogPost.tsx | 41 +++
.../BlogPostCell/BlogPostCell.mock.ts | 17 ++
.../BlogPostCell/BlogPostCell.stories.tsx | 35 +++
.../BlogPostCell/BlogPostCell.test.tsx | 42 +++
.../components/BlogPostCell/BlogPostCell.tsx | 46 ++++
.../BlogPostsCell/BlogPostsCell.mock.ts | 47 ++++
.../BlogPostsCell/BlogPostsCell.stories.tsx | 35 +++
.../BlogPostsCell/BlogPostsCell.test.tsx | 42 +++
.../BlogPostsCell/BlogPostsCell.tsx | 45 ++++
.../ClassWithClassField.stories.tsx | 26 ++
.../ClassWithClassField.test.tsx | 14 +
.../ClassWithClassField.tsx | 12 +
.../components/Contact/Contact/Contact.tsx | 98 +++++++
.../Contact/ContactCell/ContactCell.tsx | 43 ++++
.../Contact/ContactForm/ContactForm.tsx | 101 ++++++++
.../components/Contact/Contacts/Contacts.tsx | 102 ++++++++
.../Contact/ContactsCell/ContactsCell.tsx | 46 ++++
.../EditContactCell/EditContactCell.tsx | 89 +++++++
.../Contact/NewContact/NewContact.tsx | 55 ++++
.../Post/EditPostCell/EditPostCell.tsx | 78 ++++++
.../src/components/Post/NewPost/NewPost.tsx | 52 ++++
.../web/src/components/Post/Post/Post.tsx | 98 +++++++
.../src/components/Post/PostCell/PostCell.tsx | 36 +++
.../src/components/Post/PostForm/PostForm.tsx | 102 ++++++++
.../web/src/components/Post/Posts/Posts.tsx | 102 ++++++++
.../components/Post/PostsCell/PostsCell.tsx | 45 ++++
.../WaterfallBlogPostCell.mock.ts | 17 ++
.../WaterfallBlogPostCell.stories.tsx | 35 +++
.../WaterfallBlogPostCell.test.tsx | 44 ++++
.../WaterfallBlogPostCell.tsx | 67 +++++
.../esm-test-project/web/src/entry.client.tsx | 35 +++
.../esm-test-project/web/src/index.css | 13 +
.../esm-test-project/web/src/index.html | 15 ++
.../esm-test-project/web/src/layouts/.keep | 0
.../layouts/BlogLayout/BlogLayout.stories.tsx | 13 +
.../layouts/BlogLayout/BlogLayout.test.tsx | 14 +
.../web/src/layouts/BlogLayout/BlogLayout.tsx | 85 ++++++
.../layouts/ScaffoldLayout/ScaffoldLayout.tsx | 37 +++
.../web/src/lib/formatters.test.tsx | 192 ++++++++++++++
.../web/src/lib/formatters.tsx | 58 +++++
.../src/pages/AboutPage/AboutPage.stories.tsx | 13 +
.../src/pages/AboutPage/AboutPage.test.tsx | 14 +
.../web/src/pages/AboutPage/AboutPage.tsx | 10 +
.../BlogPostPage/BlogPostPage.routeHooks.ts | 5 +
.../BlogPostPage/BlogPostPage.stories.tsx | 17 ++
.../pages/BlogPostPage/BlogPostPage.test.tsx | 14 +
.../src/pages/BlogPostPage/BlogPostPage.tsx | 20 ++
.../pages/Contact/ContactPage/ContactPage.tsx | 11 +
.../Contact/ContactsPage/ContactsPage.tsx | 7 +
.../EditContactPage/EditContactPage.tsx | 11 +
.../Contact/NewContactPage/NewContactPage.tsx | 7 +
.../ContactUsPage/ContactUsPage.stories.tsx | 13 +
.../ContactUsPage/ContactUsPage.test.tsx | 14 +
.../src/pages/ContactUsPage/ContactUsPage.tsx | 133 ++++++++++
.../pages/DoublePage/DoublePage.stories.tsx | 13 +
.../src/pages/DoublePage/DoublePage.test.tsx | 14 +
.../web/src/pages/DoublePage/DoublePage.tsx | 39 +++
.../web/src/pages/DoublePage/test.png | Bin 0 -> 2500 bytes
.../pages/FatalErrorPage/FatalErrorPage.tsx | 57 ++++
.../ForgotPasswordPage/ForgotPasswordPage.tsx | 94 +++++++
.../src/pages/HomePage/HomePage.stories.tsx | 13 +
.../web/src/pages/HomePage/HomePage.test.tsx | 14 +
.../web/src/pages/HomePage/HomePage.tsx | 7 +
.../web/src/pages/LoginPage/LoginPage.tsx | 133 ++++++++++
.../src/pages/NotFoundPage/NotFoundPage.tsx | 44 ++++
.../pages/Post/EditPostPage/EditPostPage.tsx | 11 +
.../pages/Post/NewPostPage/NewPostPage.tsx | 7 +
.../web/src/pages/Post/PostPage/PostPage.tsx | 11 +
.../src/pages/Post/PostsPage/PostsPage.tsx | 7 +
.../pages/ProfilePage/ProfilePage.stories.tsx | 13 +
.../pages/ProfilePage/ProfilePage.test.tsx | 21 ++
.../web/src/pages/ProfilePage/ProfilePage.tsx | 55 ++++
.../ResetPasswordPage/ResetPasswordPage.tsx | 115 +++++++++
.../web/src/pages/SignupPage/SignupPage.tsx | 147 +++++++++++
.../WaterfallPage/WaterfallPage.routeHooks.ts | 3 +
.../WaterfallPage/WaterfallPage.stories.tsx | 17 ++
.../WaterfallPage/WaterfallPage.test.tsx | 14 +
.../src/pages/WaterfallPage/WaterfallPage.tsx | 11 +
.../esm-test-project/web/src/scaffold.css | 243 ++++++++++++++++++
.../esm-test-project/web/tsconfig.json | 44 ++++
.../esm-test-project/web/vite.config.ts | 21 ++
.../esm-test-project/web/vitest.setup.ts | 12 +
335 files changed, 11879 insertions(+)
create mode 100644 __fixtures__/esm-fragment-test-project/.editorconfig
create mode 100644 __fixtures__/esm-fragment-test-project/.env.defaults
create mode 100644 __fixtures__/esm-fragment-test-project/.env.example
create mode 100644 __fixtures__/esm-fragment-test-project/.gitignore
create mode 100644 __fixtures__/esm-fragment-test-project/.redwood/README.md
create mode 100644 __fixtures__/esm-fragment-test-project/.vscode/extensions.json
create mode 100644 __fixtures__/esm-fragment-test-project/.vscode/launch.json
create mode 100644 __fixtures__/esm-fragment-test-project/.vscode/settings.json
create mode 100644 __fixtures__/esm-fragment-test-project/.vscode/tasks.json
create mode 100644 __fixtures__/esm-fragment-test-project/.yarnrc.yml
create mode 100644 __fixtures__/esm-fragment-test-project/README.md
create mode 100644 __fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
create mode 100644 __fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
create mode 100644 __fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql
create mode 100644 __fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml
create mode 100644 __fixtures__/esm-fragment-test-project/api/db/schema.prisma
create mode 100644 __fixtures__/esm-fragment-test-project/api/package.json
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/functions/auth.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/.keep
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/lib/auth.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/lib/db.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/lib/logger.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/.keep
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/groceries.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/src/services/users/users.ts
create mode 100644 __fixtures__/esm-fragment-test-project/api/tsconfig.json
create mode 100644 __fixtures__/esm-fragment-test-project/api/vitest.config.ts
create mode 100644 __fixtures__/esm-fragment-test-project/graphql.config.cjs
create mode 100644 __fixtures__/esm-fragment-test-project/package.json
create mode 100644 __fixtures__/esm-fragment-test-project/prettier.config.cjs
create mode 100644 __fixtures__/esm-fragment-test-project/redwood.toml
create mode 100644 __fixtures__/esm-fragment-test-project/scripts/.keep
create mode 100644 __fixtures__/esm-fragment-test-project/scripts/seed.ts
create mode 100644 __fixtures__/esm-fragment-test-project/scripts/tsconfig.json
create mode 100644 __fixtures__/esm-fragment-test-project/vitest.config.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs
create mode 100644 __fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs
create mode 100644 __fixtures__/esm-fragment-test-project/web/package.json
create mode 100644 __fixtures__/esm-fragment-test-project/web/public/README.md
create mode 100644 __fixtures__/esm-fragment-test-project/web/public/favicon.png
create mode 100644 __fixtures__/esm-fragment-test-project/web/public/robots.txt
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/App.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/Routes.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/auth.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/.keep
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Card.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/entry.client.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/index.css
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/index.html
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/layouts/.keep
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/LoginPage/LoginPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Post/PostPage/PostPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/SignupPage/SignupPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
create mode 100644 __fixtures__/esm-fragment-test-project/web/src/scaffold.css
create mode 100644 __fixtures__/esm-fragment-test-project/web/tsconfig.json
create mode 100644 __fixtures__/esm-fragment-test-project/web/vite.config.ts
create mode 100644 __fixtures__/esm-fragment-test-project/web/vitest.setup.ts
create mode 100644 __fixtures__/esm-test-project/.editorconfig
create mode 100644 __fixtures__/esm-test-project/.env.defaults
create mode 100644 __fixtures__/esm-test-project/.env.example
create mode 100644 __fixtures__/esm-test-project/.gitignore
create mode 100644 __fixtures__/esm-test-project/.redwood/README.md
create mode 100644 __fixtures__/esm-test-project/.vscode/extensions.json
create mode 100644 __fixtures__/esm-test-project/.vscode/launch.json
create mode 100644 __fixtures__/esm-test-project/.vscode/settings.json
create mode 100644 __fixtures__/esm-test-project/.vscode/tasks.json
create mode 100644 __fixtures__/esm-test-project/.yarnrc.yml
create mode 100644 __fixtures__/esm-test-project/README.md
create mode 100644 __fixtures__/esm-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
create mode 100644 __fixtures__/esm-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
create mode 100644 __fixtures__/esm-test-project/api/db/migrations/migration_lock.toml
create mode 100644 __fixtures__/esm-test-project/api/db/schema.prisma
create mode 100644 __fixtures__/esm-test-project/api/package.json
create mode 100644 __fixtures__/esm-test-project/api/src/__tests__/1-db-import.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/__tests__/2-db-import.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/__tests__/3-db-import.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/__tests__/context.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.ts
create mode 100644 __fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.ts
create mode 100644 __fixtures__/esm-test-project/api/src/functions/auth.ts
create mode 100644 __fixtures__/esm-test-project/api/src/functions/graphql.ts
create mode 100644 __fixtures__/esm-test-project/api/src/graphql/.keep
create mode 100644 __fixtures__/esm-test-project/api/src/graphql/contacts.sdl.ts
create mode 100644 __fixtures__/esm-test-project/api/src/graphql/posts.sdl.ts
create mode 100644 __fixtures__/esm-test-project/api/src/graphql/users.sdl.ts
create mode 100644 __fixtures__/esm-test-project/api/src/lib/auth.ts
create mode 100644 __fixtures__/esm-test-project/api/src/lib/db.ts
create mode 100644 __fixtures__/esm-test-project/api/src/lib/logger.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/.keep
create mode 100644 __fixtures__/esm-test-project/api/src/services/contacts/contacts.scenarios.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/contacts/contacts.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/contacts/contacts.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/contacts/describeContacts.scenarios.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/contacts/describeContacts.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/posts/posts.scenarios.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/posts/posts.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/posts/posts.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/users/users.scenarios.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/users/users.test.ts
create mode 100644 __fixtures__/esm-test-project/api/src/services/users/users.ts
create mode 100644 __fixtures__/esm-test-project/api/tsconfig.json
create mode 100644 __fixtures__/esm-test-project/api/vitest-sort.config.ts
create mode 100644 __fixtures__/esm-test-project/api/vitest.config.ts
create mode 100644 __fixtures__/esm-test-project/graphql.config.cjs
create mode 100644 __fixtures__/esm-test-project/package.json
create mode 100644 __fixtures__/esm-test-project/prettier.config.cjs
create mode 100644 __fixtures__/esm-test-project/redwood.toml
create mode 100644 __fixtures__/esm-test-project/scripts/.keep
create mode 100644 __fixtures__/esm-test-project/scripts/i/am/nested.ts
create mode 100644 __fixtures__/esm-test-project/scripts/one/two/myNestedScript.ts
create mode 100644 __fixtures__/esm-test-project/scripts/seed.ts
create mode 100644 __fixtures__/esm-test-project/scripts/tsconfig.json
create mode 100644 __fixtures__/esm-test-project/vitest.config.ts
create mode 100644 __fixtures__/esm-test-project/web/config/postcss.config.cjs
create mode 100644 __fixtures__/esm-test-project/web/config/tailwind.config.cjs
create mode 100644 __fixtures__/esm-test-project/web/package.json
create mode 100644 __fixtures__/esm-test-project/web/public/README.md
create mode 100644 __fixtures__/esm-test-project/web/public/favicon.png
create mode 100644 __fixtures__/esm-test-project/web/public/robots.txt
create mode 100644 __fixtures__/esm-test-project/web/src/App.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/Redwood.stories.mdx
create mode 100644 __fixtures__/esm-test-project/web/src/Routes.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/auth.ts
create mode 100644 __fixtures__/esm-test-project/web/src/components/.keep
create mode 100644 __fixtures__/esm-test-project/web/src/components/Author/Author.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Author/Author.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Author/Author.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
create mode 100644 __fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/Contact/Contact.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/Contacts/Contacts.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Contact/NewContact/NewContact.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/NewPost/NewPost.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/Post/Post.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/PostCell/PostCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/PostForm/PostForm.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/Posts/Posts.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
create mode 100644 __fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/entry.client.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/index.css
create mode 100644 __fixtures__/esm-test-project/web/src/index.html
create mode 100644 __fixtures__/esm-test-project/web/src/layouts/.keep
create mode 100644 __fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/lib/formatters.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/lib/formatters.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
create mode 100644 __fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/DoublePage/test.png
create mode 100644 __fixtures__/esm-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/LoginPage/LoginPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Post/PostPage/PostPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/SignupPage/SignupPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
create mode 100644 __fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
create mode 100644 __fixtures__/esm-test-project/web/src/scaffold.css
create mode 100644 __fixtures__/esm-test-project/web/tsconfig.json
create mode 100644 __fixtures__/esm-test-project/web/vite.config.ts
create mode 100644 __fixtures__/esm-test-project/web/vitest.setup.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2aa5cae0ab..4d44a020d8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -267,6 +267,15 @@ jobs:
yarn rw test api --no-watch
working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }}
+ - name: Run db import tracking tests
+ shell: bash
+ run: |
+ # Only run this for ESM projects where the vitest config file exists
+ if test -f ./vitest-sort.config.ts; then
+ npx vitest --config ./vitest-sort.config.ts run
+ fi
+ working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }}/api
+
- name: 🖥️ Run serve smoke tests
working-directory: tasks/smoke-tests/serve
run: npx playwright test
diff --git a/__fixtures__/esm-fragment-test-project/.editorconfig b/__fixtures__/esm-fragment-test-project/.editorconfig
new file mode 100644
index 0000000000..ae10a5cce3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/__fixtures__/esm-fragment-test-project/.env.defaults b/__fixtures__/esm-fragment-test-project/.env.defaults
new file mode 100644
index 0000000000..fb88fb33b3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.env.defaults
@@ -0,0 +1,19 @@
+# These environment variables will be used by default if you do not create any
+# yourself in .env. This file should be safe to check into your version control
+# system. Any custom values should go in .env and .env should *not* be checked
+# into version control.
+
+# schema.prisma defaults
+DATABASE_URL=file:./dev.db
+
+# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
+# TEST_DATABASE_URL=file:./.redwood/test.db
+
+# disables Prisma CLI update notifier
+PRISMA_HIDE_UPDATE_MESSAGE=true
+
+# Option to override the current environment's default api-side log level
+# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise.
+# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
+# Ordered by how verbose they are: trace | debug | info | warn | error | silent
+# LOG_LEVEL=debug
diff --git a/__fixtures__/esm-fragment-test-project/.env.example b/__fixtures__/esm-fragment-test-project/.env.example
new file mode 100644
index 0000000000..2a2de6c026
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.env.example
@@ -0,0 +1,4 @@
+# DATABASE_URL=file:./dev.db
+# TEST_DATABASE_URL=file:./.redwood/test.db
+# PRISMA_HIDE_UPDATE_MESSAGE=true
+# LOG_LEVEL=trace
diff --git a/__fixtures__/esm-fragment-test-project/.gitignore b/__fixtures__/esm-fragment-test-project/.gitignore
new file mode 100644
index 0000000000..31d9637ede
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.gitignore
@@ -0,0 +1,24 @@
+.idea
+.DS_Store
+.env*
+!.env.example
+!.env.defaults
+.netlify
+.redwood/*
+!.redwood/README.md
+dev.db*
+dist
+dist-babel
+node_modules
+yarn-error.log
+web/public/mockServiceWorker.js
+web/types/graphql.d.ts
+api/types/graphql.d.ts
+api/src/lib/generateGraphiQLHeader.*
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/__fixtures__/esm-fragment-test-project/.redwood/README.md b/__fixtures__/esm-fragment-test-project/.redwood/README.md
new file mode 100644
index 0000000000..8a1bf5738b
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.redwood/README.md
@@ -0,0 +1,44 @@
+# .redwood
+
+## What is this directory?
+
+Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project.
+
+## Do I need to do anything with this directory?
+
+No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood.
+
+You don't need to commit any other contents of this directory to your version control system. It's ignored by default.
+
+## What's in this directory?
+
+### Files
+
+| Name | Description |
+| :---------------- | :----------------------------------------------------------------------------------------------------------------- |
+| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. |
+| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. |
+| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. |
+| test.db | The sqlite database used when running tests. |
+
+### Directories
+
+| Name | Description |
+| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
+| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. |
+| logs | Stores log files for background tasks such as update checking. |
+| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. |
+| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. |
+| types | Stores the results of type generation. |
+| updateCheck | Stores a file which contains the results of checking for Redwood updates. |
+| studio | Used to store data for `rw studio` |
+
+We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list.
+
+### Telemetry
+
+RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com).
+
+### Have any questions?
+
+Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions.
diff --git a/__fixtures__/esm-fragment-test-project/.vscode/extensions.json b/__fixtures__/esm-fragment-test-project/.vscode/extensions.json
new file mode 100644
index 0000000000..6e458a9231
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.vscode/extensions.json
@@ -0,0 +1,16 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ofhumanbondage.react-proptypes-intellisense",
+ "mgmcdermott.vscode-language-babel",
+ "wix.vscode-import-cost",
+ "pflannery.vscode-versionlens",
+ "editorconfig.editorconfig",
+ "prisma.prisma",
+ "graphql.vscode-graphql",
+ "csstools.postcss",
+ "bradlc.vscode-tailwindcss"
+ ],
+ "unwantedRecommendations": []
+}
\ No newline at end of file
diff --git a/__fixtures__/esm-fragment-test-project/.vscode/launch.json b/__fixtures__/esm-fragment-test-project/.vscode/launch.json
new file mode 100644
index 0000000000..37257c5c15
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.vscode/launch.json
@@ -0,0 +1,56 @@
+{
+ "version": "0.3.0",
+ "configurations": [
+ {
+ "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening
+ "name": "Run Dev Server",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "name": "Attach API debugger",
+ "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration
+ "request": "attach",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node",
+ "localRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "remoteRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "sourceMaps": true,
+ "restart": true,
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "name": "Launch Web debugger",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:8910",
+ "webRoot": "${workspaceRoot}/web/src",
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "command": "yarn redwood test api",
+ "name": "Test api",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "command": "yarn redwood test web",
+ "name": "Test web",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ ],
+ "compounds": [
+ {
+ "name": "Start Debug",
+ "configurations": [
+ "Run Dev Server",
+ "Attach API debugger",
+ "Launch Web debugger"
+ ],
+ "stopAll": true
+ }
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/.vscode/settings.json b/__fixtures__/esm-fragment-test-project/.vscode/settings.json
new file mode 100644
index 0000000000..1d3afa8ae3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.vscode/settings.json
@@ -0,0 +1,17 @@
+{
+ "editor.tabSize": 2,
+ "files.trimTrailingWhitespace": true,
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ },
+ "[prisma]": {
+ "editor.formatOnSave": true
+ },
+ "tailwindCSS.classAttributes": [
+ "class",
+ "className",
+ "activeClassName",
+ "errorClassName"
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/.vscode/tasks.json b/__fixtures__/esm-fragment-test-project/.vscode/tasks.json
new file mode 100644
index 0000000000..549249ec63
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "WaitForDevServer",
+ "group": "none",
+ "type": "shell",
+ "command": "bash",
+ "args": [
+ "-c",
+ "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;"
+ ],
+ "windows": {
+ "command": "powershell",
+ "args": [
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };"
+ ]
+ },
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "panel": "shared",
+ "close": true
+ }
+ },
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/.yarnrc.yml b/__fixtures__/esm-fragment-test-project/.yarnrc.yml
new file mode 100644
index 0000000000..e8c5d50aa7
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/.yarnrc.yml
@@ -0,0 +1,15 @@
+# Yarn's manifest file. You can configure yarn here.
+# See https://yarnpkg.com/configuration/yarnrc.
+
+# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option.
+compressionLevel: 0
+
+enableGlobalCache: true
+
+# Lets yarn use hardlinks inside `node_modules` to dedupe packages.
+# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store.
+nmMode: hardlinks-local
+
+# How to install Node packages.
+# Heads up: right now, Redwood expects this to be `node-modules`.
+nodeLinker: node-modules
diff --git a/__fixtures__/esm-fragment-test-project/README.md b/__fixtures__/esm-fragment-test-project/README.md
new file mode 100644
index 0000000000..4487e7f63d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/README.md
@@ -0,0 +1,17 @@
+# README
+
+Welcome to your new [CedarJS](https://cedarjs.com) project!
+
+Start by installing dependencies:
+
+```
+yarn install
+```
+
+Then start the development server:
+
+```
+yarn redwood dev
+```
+
+Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources.
diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
new file mode 100644
index 0000000000..9dd73df9b6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
@@ -0,0 +1,34 @@
+-- CreateTable
+CREATE TABLE "UserExample" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "name" TEXT
+);
+
+-- CreateTable
+CREATE TABLE "Post" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "title" TEXT NOT NULL,
+ "body" TEXT NOT NULL,
+ "authorId" INTEGER NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "fullName" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" DATETIME,
+ "roles" TEXT
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
new file mode 100644
index 0000000000..8d7bd91beb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
@@ -0,0 +1,8 @@
+-- CreateTable
+CREATE TABLE "Contact" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql
new file mode 100644
index 0000000000..f7f6352f3e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql
@@ -0,0 +1,28 @@
+-- CreateTable
+CREATE TABLE "Produce" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "quantity" INTEGER NOT NULL,
+ "price" INTEGER NOT NULL,
+ "nutrients" TEXT,
+ "region" TEXT NOT NULL,
+ "isSeedless" BOOLEAN,
+ "ripenessIndicators" TEXT,
+ "vegetableFamily" TEXT,
+ "isPickled" BOOLEAN,
+ "stallId" TEXT NOT NULL,
+ CONSTRAINT "Produce_stallId_fkey" FOREIGN KEY ("stallId") REFERENCES "Stall" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Stall" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "stallNumber" TEXT NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Produce_name_key" ON "Produce"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Stall_stallNumber_key" ON "Stall"("stallNumber");
diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml b/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml
new file mode 100644
index 0000000000..e5e5c4705a
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite"
\ No newline at end of file
diff --git a/__fixtures__/esm-fragment-test-project/api/db/schema.prisma b/__fixtures__/esm-fragment-test-project/api/db/schema.prisma
new file mode 100644
index 0000000000..bf7e70169a
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/db/schema.prisma
@@ -0,0 +1,79 @@
+// Don't forget to tell Prisma about your edits to this file using
+// `yarn rw prisma migrate dev` or `yarn rw prisma db push`.
+// `migrate` is like committing while `push` is for prototyping.
+// Read more about both here:
+// https://www.prisma.io/docs/orm/prisma-migrate
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+// Define your own datamodels here and run `yarn redwood prisma migrate dev`
+// to create migrations for them and apply to your dev DB.
+// TODO: Please remove the following example:
+model UserExample {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ authorId Int
+ author User @relation(fields: [authorId], references: [id])
+ createdAt DateTime @default(now())
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ hashedPassword String
+ fullName String
+ salt String
+ resetToken String?
+ resetTokenExpiresAt DateTime?
+ roles String?
+ posts Post[]
+}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
+}
+
+model Produce {
+ id String @id @default(cuid())
+ name String @unique
+ quantity Int
+ price Int
+ nutrients String?
+ region String
+ /// Available only for fruits
+ isSeedless Boolean?
+ /// Available only for fruits
+ ripenessIndicators String?
+ /// Available only for vegetables
+ vegetableFamily String?
+ /// Available only for vegetables
+ isPickled Boolean?
+ stall Stall @relation(fields: [stallId], references: [id], onDelete: Cascade)
+ stallId String
+}
+
+model Stall {
+ id String @id @default(cuid())
+ name String
+ stallNumber String @unique
+ produce Produce[]
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/package.json b/__fixtures__/esm-fragment-test-project/api/package.json
new file mode 100644
index 0000000000..f3b9e4e1cb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "api",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@cedarjs/api": "0.0.5",
+ "@cedarjs/auth-dbauth-api": "0.0.5",
+ "@cedarjs/graphql-server": "0.0.5"
+ }
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts b/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts
new file mode 100644
index 0000000000..972c4756e8
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts
@@ -0,0 +1,14 @@
+test('Set a mock user on the context', async () => {
+ const user = {
+ id: 0o7,
+ name: 'Bond, James Bond',
+ email: 'totallyNotASpy@example.com',
+ roles: 'secret_agent',
+ }
+ mockCurrentUser(user)
+ expect(context.currentUser).toStrictEqual(user)
+})
+
+test('Context is isolated between tests', () => {
+ expect(context).toStrictEqual({})
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
new file mode 100644
index 0000000000..83b683768b
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts
@@ -0,0 +1,20 @@
+import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
+
+import requireAuth from './requireAuth.js'
+
+describe('requireAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(requireAuth.schema).toBeTruthy()
+ expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
+ })
+
+ it('requireAuth has stub implementation. Should not throw when current user', () => {
+ // If you want to set values in context, pass it through e.g.
+ // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
+ const mockExecution = mockRedwoodDirective(requireAuth, {
+ context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } },
+ })
+
+ expect(mockExecution).not.toThrowError()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts
new file mode 100644
index 0000000000..3dadf21e68
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts
@@ -0,0 +1,25 @@
+import { gql } from 'graphql-tag'
+
+import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+import { requireAuth as applicationRequireAuth } from 'src/lib/auth.js'
+
+export const schema = gql`
+ """
+ Use to check whether or not a user is authenticated and is associated
+ with an optional set of roles.
+ """
+ directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+`
+
+type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
+
+const validate: RequireAuthValidate = ({ directiveArgs }) => {
+ const { roles } = directiveArgs
+ applicationRequireAuth({ roles })
+}
+
+const requireAuth = createValidatorDirective(schema, validate)
+
+export default requireAuth
diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts
new file mode 100644
index 0000000000..68c006bdae
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts
@@ -0,0 +1,10 @@
+import { getDirectiveName } from '@cedarjs/testing/api'
+
+import skipAuth from './skipAuth.js'
+
+describe('skipAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(skipAuth.schema).toBeTruthy()
+ expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts
new file mode 100644
index 0000000000..fdea5cf17b
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts
@@ -0,0 +1,16 @@
+import { gql } from 'graphql-tag'
+
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+export const schema = gql`
+ """
+ Use to skip authentication checks and allow public access.
+ """
+ directive @skipAuth on FIELD_DEFINITION
+`
+
+const skipAuth = createValidatorDirective(schema, () => {
+ return
+})
+
+export default skipAuth
diff --git a/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts b/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts
new file mode 100644
index 0000000000..b2192a0dcf
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts
@@ -0,0 +1,202 @@
+import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
+
+import { DbAuthHandler } from '@cedarjs/auth-dbauth-api'
+import type { DbAuthHandlerOptions, UserType } from '@cedarjs/auth-dbauth-api'
+
+import { cookieName } from 'src/lib/auth.js'
+import { db } from 'src/lib/db.js'
+
+export const handler = async (
+ event: APIGatewayProxyEvent,
+ context: Context
+) => {
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
+ // handler() is invoked after verifying that a user was found with the given
+ // username. This is where you can send the user an email with a link to
+ // reset their password. With the default dbAuth routes and field names, the
+ // URL to reset the password will be:
+ //
+ // https://example.com/reset-password?resetToken=${user.resetToken}
+ //
+ // Whatever is returned from this function will be returned from
+ // the `forgotPassword()` function that is destructured from `useAuth()`.
+ // You could use this return value to, for example, show the email
+ // address in a toast message so the user will know it worked and where
+ // to look for the email.
+ //
+ // Note that this return value is sent to the client in *plain text*
+ // so don't include anything you wouldn't want prying eyes to see. The
+ // `user` here has been sanitized to only include the fields listed in
+ // `allowedUserFields` so it should be safe to return as-is.
+ handler: (user, _resetToken) => {
+ // TODO: Send user an email/message with a link to reset their password,
+ // including the `resetToken`. The URL should look something like:
+ // `http://localhost:8910/reset-password?resetToken=${resetToken}`
+
+ return user
+ },
+
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
+ expires: 60 * 60 * 24,
+
+ errors: {
+ // for security reasons you may want to be vague here rather than expose
+ // the fact that the email address wasn't found (prevents fishing for
+ // valid email addresses)
+ usernameNotFound: 'Username not found',
+ // if the user somehow gets around client validation
+ usernameRequired: 'Username is required',
+ },
+ }
+
+ const loginOptions: DbAuthHandlerOptions['login'] = {
+ // handler() is called after finding the user that matches the
+ // username/password provided at login, but before actually considering them
+ // logged in. The `user` argument will be the user in the database that
+ // matched the username/password.
+ //
+ // If you want to allow this user to log in simply return the user.
+ //
+ // If you want to prevent someone logging in for another reason (maybe they
+ // didn't validate their email yet), throw an error and it will be returned
+ // by the `logIn()` function from `useAuth()` in the form of:
+ // `{ message: 'Error message' }`
+ handler: (user) => {
+ return user
+ },
+
+ errors: {
+ usernameOrPasswordMissing: 'Both username and password are required',
+ usernameNotFound: 'Username ${username} not found',
+ // For security reasons you may want to make this the same as the
+ // usernameNotFound error so that a malicious user can't use the error
+ // to narrow down if it's the username or password that's incorrect
+ incorrectPassword: 'Incorrect password for ${username}',
+ },
+
+ // How long a user will remain logged in, in seconds
+ expires: 60 * 60 * 24 * 365 * 10,
+ }
+
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
+ // handler() is invoked after the password has been successfully updated in
+ // the database. Returning anything truthy will automatically log the user
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
+ // user to the login page.
+ handler: (_user) => {
+ return true
+ },
+
+ // If `false` then the new password MUST be different from the current one
+ allowReusedPassword: true,
+
+ errors: {
+ // the resetToken is valid, but expired
+ resetTokenExpired: 'resetToken is expired',
+ // no user was found with the given resetToken
+ resetTokenInvalid: 'resetToken is invalid',
+ // the resetToken was not present in the URL
+ resetTokenRequired: 'resetToken is required',
+ // new password is the same as the old password (apparently they did not forget it)
+ reusedPassword: 'Must choose a new password',
+ },
+ }
+
+ interface UserAttributes {
+ 'full-name': string
+ }
+
+ const signupOptions: DbAuthHandlerOptions<
+ UserType,
+ UserAttributes
+ >['signup'] = {
+ // Whatever you want to happen to your data on new user signup. Redwood will
+ // check for duplicate usernames before calling this handler. At a minimum
+ // you need to save the `username`, `hashedPassword` and `salt` to your
+ // user table. `userAttributes` contains any additional object members that
+ // were included in the object given to the `signUp()` function you got
+ // from `useAuth()`.
+ //
+ // If you want the user to be immediately logged in, return the user that
+ // was created.
+ //
+ // If this handler throws an error, it will be returned by the `signUp()`
+ // function in the form of: `{ error: 'Error message' }`.
+ //
+ // If this returns anything else, it will be returned by the
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
+ handler: ({ username, hashedPassword, salt, userAttributes }) => {
+ return db.user.create({
+ data: {
+ email: username,
+ hashedPassword: hashedPassword,
+ salt: salt,
+ fullName: userAttributes['full-name'],
+ },
+ })
+ },
+
+ // Include any format checks for password here. Return `true` if the
+ // password is valid, otherwise throw a `PasswordValidationError`.
+ // Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
+ passwordValidation: (_password) => {
+ return true
+ },
+
+ errors: {
+ // `field` will be either "username" or "password"
+ fieldMissing: '${field} is required',
+ usernameTaken: 'Username `${username}` already in use',
+ },
+ }
+
+ const authHandler = new DbAuthHandler(event, context, {
+ // Provide prisma db client
+ db: db,
+
+ // The name of the property you'd call on `db` to access your user table.
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
+ authModelAccessor: 'user',
+
+ // A map of what dbAuth calls a field to what your database calls it.
+ // `id` is whatever column you use to uniquely identify a user (probably
+ // something like `id` or `userId` or even `email`)
+ authFields: {
+ id: 'id',
+ username: 'email',
+ hashedPassword: 'hashedPassword',
+ salt: 'salt',
+ resetToken: 'resetToken',
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
+ },
+
+ // A list of fields on your user object that are safe to return to the
+ // client when invoking a handler that returns a user (like forgotPassword
+ // and signup). This list should be as small as possible to be sure not to
+ // leak any sensitive information to the client.
+ allowedUserFields: ['id', 'email'],
+
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
+ cookie: {
+ attributes: {
+ HttpOnly: true,
+ Path: '/',
+ SameSite: 'Lax',
+ Secure: process.env.NODE_ENV !== 'development',
+
+ // If you need to allow other domains (besides the api side) access to
+ // the dbAuth session cookie:
+ // Domain: 'example.com',
+ },
+ name: cookieName,
+ },
+
+ forgotPassword: forgotPasswordOptions,
+ login: loginOptions,
+ resetPassword: resetPasswordOptions,
+ signup: signupOptions,
+ })
+
+ return await authHandler.invoke()
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts b/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts
new file mode 100644
index 0000000000..4445ef3976
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts
@@ -0,0 +1,25 @@
+import { createAuthDecoder } from '@cedarjs/auth-dbauth-api'
+import { createGraphQLHandler } from '@cedarjs/graphql-server'
+
+import directives from 'src/directives/**/*.{js,ts}'
+import sdls from 'src/graphql/**/*.sdl.{js,ts}'
+import services from 'src/services/**/*.{js,ts}'
+
+import { cookieName, getCurrentUser } from 'src/lib/auth.js'
+import { db } from 'src/lib/db.js'
+import { logger } from 'src/lib/logger.js'
+
+const authDecoder = createAuthDecoder(cookieName)
+
+export const handler = createGraphQLHandler({
+ authDecoder,
+ getCurrentUser,
+ loggerConfig: { logger, options: {} },
+ directives,
+ sdls,
+ services,
+ onException: () => {
+ // Disconnect from your database with an unhandled exception.
+ db.$disconnect()
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/.keep b/__fixtures__/esm-fragment-test-project/api/src/graphql/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts
new file mode 100644
index 0000000000..7dec262a57
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts
@@ -0,0 +1,32 @@
+export const schema = gql`
+ type Contact {
+ id: Int!
+ name: String!
+ email: String!
+ message: String!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ contacts: [Contact!]! @requireAuth
+ contact(id: Int!): Contact @requireAuth
+ }
+
+ input CreateContactInput {
+ name: String!
+ email: String!
+ message: String!
+ }
+
+ input UpdateContactInput {
+ name: String
+ email: String
+ message: String
+ }
+
+ type Mutation {
+ createContact(input: CreateContactInput!): Contact @skipAuth
+ updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth
+ deleteContact(id: Int!): Contact! @requireAuth(roles: ["ADMIN"])
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts
new file mode 100644
index 0000000000..0870d7daeb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts
@@ -0,0 +1,49 @@
+export const schema = gql`
+ interface Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ }
+
+ type Fruit implements Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ "Seedless is only for fruits"
+ isSeedless: Boolean
+ "Ripeness is only for fruits"
+ ripenessIndicators: String
+ }
+
+ type Vegetable implements Grocery {
+ id: ID!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ stall: Stall!
+ region: String!
+ "Veggie Family is only for vegetables"
+ vegetableFamily: String
+ "Pickled is only for vegetables"
+ isPickled: Boolean
+ }
+
+ union Groceries = Fruit | Vegetable
+
+ type Query {
+ groceries: [Groceries!]! @skipAuth
+ fruits: [Fruit!]! @skipAuth
+ fruitById(id: ID!): Fruit @skipAuth
+ vegetables: [Vegetable!]! @skipAuth
+ vegetableById(id: ID!): Vegetable @skipAuth
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts
new file mode 100644
index 0000000000..09cf9b2cc6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts
@@ -0,0 +1,33 @@
+export const schema = gql`
+ type Post {
+ id: Int!
+ title: String!
+ body: String!
+ authorId: Int!
+ author: User!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ posts: [Post!]! @skipAuth
+ post(id: Int!): Post @skipAuth
+ }
+
+ input CreatePostInput {
+ title: String!
+ body: String!
+ authorId: Int!
+ }
+
+ input UpdatePostInput {
+ title: String
+ body: String
+ authorId: Int
+ }
+
+ type Mutation {
+ createPost(input: CreatePostInput!): Post! @requireAuth
+ updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
+ deletePost(id: Int!): Post! @requireAuth
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts
new file mode 100644
index 0000000000..1a3342f662
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts
@@ -0,0 +1,54 @@
+export const schema = gql`
+ type Produce {
+ id: String!
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ region: String!
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stall: Stall!
+ stallId: String!
+ }
+
+ type Query {
+ produces: [Produce!]! @skipAuth
+ produce(id: String!): Produce @skipAuth
+ }
+
+ input CreateProduceInput {
+ name: String!
+ quantity: Int!
+ price: Int!
+ nutrients: String
+ region: String!
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stallId: String!
+ }
+
+ input UpdateProduceInput {
+ name: String
+ quantity: Int
+ price: Int
+ nutrients: String
+ region: String
+ isSeedless: Boolean
+ ripenessIndicators: String
+ vegetableFamily: String
+ isPickled: Boolean
+ stallId: String
+ }
+
+ type Mutation {
+ createProduce(input: CreateProduceInput!): Produce! @skipAuth
+ updateProduce(id: String!, input: UpdateProduceInput!): Produce!
+ @skipAuth
+ deleteProduce(id: String!): Produce! @skipAuth
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts
new file mode 100644
index 0000000000..c934eeb4a5
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts
@@ -0,0 +1,29 @@
+export const schema = gql`
+ type Stall {
+ id: String!
+ name: String!
+ stallNumber: String!
+ produce: [Produce]!
+ }
+
+ type Query {
+ stalls: [Stall!]! @requireAuth
+ stall(id: String!): Stall @requireAuth
+ }
+
+ input CreateStallInput {
+ name: String!
+ stallNumber: String!
+ }
+
+ input UpdateStallInput {
+ name: String
+ stallNumber: String
+ }
+
+ type Mutation {
+ createStall(input: CreateStallInput!): Stall! @requireAuth
+ updateStall(id: String!, input: UpdateStallInput!): Stall! @requireAuth
+ deleteStall(id: String!): Stall! @requireAuth
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts
new file mode 100644
index 0000000000..e2d1c0bed1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts
@@ -0,0 +1,25 @@
+export const schema = gql`
+ type User {
+ id: Int!
+ email: String!
+ fullName: String!
+ roles: String
+ posts: [Post]!
+ }
+
+ type Query {
+ user(id: Int!): User @skipAuth
+ }
+
+ input CreateUserInput {
+ email: String!
+ fullName: String!
+ roles: String
+ }
+
+ input UpdateUserInput {
+ email: String
+ fullName: String
+ roles: String
+ }
+`
diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts
new file mode 100644
index 0000000000..e7eafb1ef6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts
@@ -0,0 +1,121 @@
+import type { Decoded } from '@cedarjs/api'
+import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server'
+
+import { db } from './db.js'
+
+/**
+ * The name of the cookie that dbAuth sets
+ *
+ * %port% will be replaced with the port the api server is running on.
+ * If you have multiple RW apps running on the same host, you'll need to
+ * make sure they all use unique cookie names
+ */
+export const cookieName = 'session_%port%'
+
+/**
+ * The session object sent in as the first argument to getCurrentUser() will
+ * have a single key `id` containing the unique ID of the logged in user
+ * (whatever field you set as `authFields.id` in your auth function config).
+ * You'll need to update the call to `db` below if you use a different model
+ * name or unique field name, for example:
+ *
+ * return await db.profile.findUnique({ where: { email: session.id } })
+ * ───┬─── ──┬──
+ * model accessor ─┘ unique id field name ─┘
+ *
+ * !! BEWARE !! Anything returned from this function will be available to the
+ * client--it becomes the content of `currentUser` on the web side (as well as
+ * `context.currentUser` on the api side). You should carefully add additional
+ * fields to the `select` object below once you've decided they are safe to be
+ * seen if someone were to open the Web Inspector in their browser.
+ */
+export const getCurrentUser = async (session: Decoded) => {
+ if (!session || typeof session.id !== 'number') {
+ throw new Error('Invalid session')
+ }
+
+ return await db.user.findUnique({
+ where: { id: session.id },
+ select: { id: true, roles: true, email: true },
+ })
+}
+
+/**
+ * The user is authenticated if there is a currentUser in the context
+ *
+ * @returns {boolean} - If the currentUser is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return !!context.currentUser
+}
+
+/**
+ * When checking role membership, roles can be a single value, a list, or none.
+ * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
+ */
+type AllowedRoles = string | string[] | undefined
+
+/**
+ * Checks if the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
+ *
+ * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
+ * or when no roles are provided to check against. Otherwise returns false.
+ */
+export const hasRole = (roles: AllowedRoles): boolean => {
+ if (!isAuthenticated()) {
+ return false
+ }
+
+ const currentUserRoles = context.currentUser?.roles as string | string[]
+
+ if (typeof roles === 'string') {
+ if (typeof currentUserRoles === 'string') {
+ // roles to check is a string, currentUser.roles is a string
+ return currentUserRoles === roles
+ } else if (Array.isArray(currentUserRoles)) {
+ // roles to check is a string, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) => roles === allowedRole)
+ }
+ }
+
+ if (Array.isArray(roles)) {
+ if (Array.isArray(currentUserRoles)) {
+ // roles to check is an array, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) =>
+ roles.includes(allowedRole)
+ )
+ } else if (typeof currentUserRoles === 'string') {
+ // roles to check is an array, currentUser.roles is a string
+ return roles.some((allowedRole) => currentUserRoles === allowedRole)
+ }
+ }
+
+ // roles not found
+ return false
+}
+
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
+ *
+ * @returns - If the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @throws {@link AuthenticationError} - If the currentUser is not authenticated
+ * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
+ *
+ * @see https://github.com/cedarjs/cedar/tree/main/packages/auth for examples
+ */
+export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
+ if (!isAuthenticated()) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
+
+ if (roles && !hasRole(roles)) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts
new file mode 100644
index 0000000000..b6fa93228f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts
@@ -0,0 +1,26 @@
+// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor
+// for options.
+
+import { PrismaClient } from '@prisma/client'
+
+import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'
+
+import { logger } from './logger.js'
+
+const prismaClient = new PrismaClient({
+ log: emitLogLevels(['info', 'warn', 'error']),
+})
+
+handlePrismaLogging({
+ db: prismaClient,
+ logger,
+ logLevels: ['info', 'warn', 'error'],
+})
+
+/**
+ * Global Prisma client extensions should be added here, as $extend
+ * returns a new instance.
+ * export const db = prismaClient.$extend(...)
+ * Add any .$on hooks before using $extend
+ */
+export const db = prismaClient
diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts
new file mode 100644
index 0000000000..b5f38c1921
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts
@@ -0,0 +1,17 @@
+import { createLogger } from '@cedarjs/api/logger'
+
+/**
+ * Creates a logger with RedwoodLoggerOptions
+ *
+ * These extend and override default LoggerOptions,
+ * can define a destination like a file or other supported pino log transport stream,
+ * and sets whether or not to show the logger configuration settings (defaults to false)
+ *
+ * @param RedwoodLoggerOptions
+ *
+ * RedwoodLoggerOptions have
+ * @param {options} LoggerOptions - defines how to log, such as redaction and format
+ * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file
+ * @param {boolean} showConfig - whether to display logger configuration on initialization
+ */
+export const logger = createLogger({})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/.keep b/__fixtures__/esm-fragment-test-project/api/src/services/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
new file mode 100644
index 0000000000..99271967d8
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Contact } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ contact: {
+ one: { data: { name: 'String', email: 'String', message: 'String' } },
+ two: { data: { name: 'String', email: 'String', message: 'String' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts
new file mode 100644
index 0000000000..bec7102a29
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts
@@ -0,0 +1,59 @@
+import type { Contact } from '@prisma/client'
+
+import {
+ contacts,
+ contact,
+ createContact,
+ updateContact,
+ deleteContact,
+} from './contacts.js'
+import type { StandardScenario } from './contacts.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('contacts', () => {
+ scenario('returns all contacts', async (scenario: StandardScenario) => {
+ const result = await contacts()
+
+ expect(result.length).toEqual(Object.keys(scenario.contact).length)
+ })
+
+ scenario('returns a single contact', async (scenario: StandardScenario) => {
+ const result = await contact({ id: scenario.contact.one.id })
+
+ expect(result).toEqual(scenario.contact.one)
+ })
+
+ scenario('creates a contact', async () => {
+ const result = await createContact({
+ input: { name: 'String', email: 'String', message: 'String' },
+ })
+
+ expect(result.name).toEqual('String')
+ expect(result.email).toEqual('String')
+ expect(result.message).toEqual('String')
+ })
+
+ scenario('updates a contact', async (scenario: StandardScenario) => {
+ const original = (await contact({ id: scenario.contact.one.id })) as Contact
+ const result = await updateContact({
+ id: original.id,
+ input: { name: 'String2' },
+ })
+
+ expect(result.name).toEqual('String2')
+ })
+
+ scenario('deletes a contact', async (scenario: StandardScenario) => {
+ const original = (await deleteContact({
+ id: scenario.contact.one.id,
+ })) as Contact
+ const result = await contact({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts
new file mode 100644
index 0000000000..88b001d48a
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts
@@ -0,0 +1,37 @@
+import type { QueryResolvers, MutationResolvers } from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const contacts: QueryResolvers['contacts'] = () => {
+ return db.contact.findMany()
+}
+
+export const contact: QueryResolvers['contact'] = ({ id }) => {
+ return db.contact.findUnique({
+ where: { id },
+ })
+}
+
+export const createContact: MutationResolvers['createContact'] = ({
+ input,
+}) => {
+ return db.contact.create({
+ data: input,
+ })
+}
+
+export const updateContact: MutationResolvers['updateContact'] = ({
+ id,
+ input,
+}) => {
+ return db.contact.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteContact: MutationResolvers['deleteContact'] = ({ id }) => {
+ return db.contact.delete({
+ where: { id },
+ })
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts
new file mode 100644
index 0000000000..99271967d8
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Contact } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ contact: {
+ one: { data: { name: 'String', email: 'String', message: 'String' } },
+ two: { data: { name: 'String', email: 'String', message: 'String' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts
new file mode 100644
index 0000000000..ad9a697c07
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts
@@ -0,0 +1,57 @@
+import { db } from 'src/lib/db.js'
+
+import { contact, contacts, createContact } from './contacts.js'
+import type { StandardScenario } from './contacts.scenarios.js'
+
+/**
+ * Example test for describe scenario.
+ *
+ * Note that scenario tests need a matching [name].scenarios.ts file.
+ */
+
+describeScenario('contacts', (getScenario) => {
+ let scenario: StandardScenario
+
+ beforeEach(() => {
+ scenario = getScenario()
+ })
+
+ it('returns all contacts', async () => {
+ const result = await contacts()
+
+ expect(result.length).toEqual(Object.keys(scenario.contact).length)
+ })
+
+ it('returns a single contact', async () => {
+ const result = await contact({ id: scenario.contact.one.id })
+
+ expect(result).toEqual(scenario.contact.one)
+ })
+
+ it('creates a contact', async () => {
+ const result = await createContact({
+ input: {
+ name: 'Bazinga',
+ email: 'contact@describe.scenario',
+ message: 'Describe scenario works!',
+ },
+ })
+
+ expect(result.name).toEqual('Bazinga')
+ expect(result.email).toEqual('contact@describe.scenario')
+ expect(result.message).toEqual('Describe scenario works!')
+ })
+
+ it('Checking that describe scenario works', async () => {
+ // This test is dependent on the above test. If you used a normal scenario it would not work
+ const contactCreatedInAboveTest = await db.contact.findFirst({
+ where: {
+ email: 'contact@describe.scenario',
+ },
+ })
+
+ expect(contactCreatedInAboveTest.message).toEqual(
+ 'Describe scenario works!'
+ )
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts b/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts
new file mode 100644
index 0000000000..fbfa9ac9f2
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts
@@ -0,0 +1,32 @@
+import type { Produce } from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+const isFruit = (grocery: Produce) => {
+ return grocery.isSeedless !== null && grocery.ripenessIndicators !== null
+}
+
+export const groceries = async () => {
+ const result = await db.produce.findMany({
+ include: { stall: true },
+ orderBy: { name: 'asc' },
+ })
+
+ const avail = result.map((grocery) => {
+ if (isFruit(grocery)) {
+ return {
+ ...grocery,
+ __typename: 'Fruit',
+ __resolveType: 'Fruit',
+ }
+ } else {
+ return {
+ ...grocery,
+ __typename: 'Vegetable',
+ __resolveType: 'Vegetable',
+ }
+ }
+ })
+
+ return avail
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts
new file mode 100644
index 0000000000..6890e49ad1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts
@@ -0,0 +1,38 @@
+import type { Prisma, Post } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ post: {
+ one: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String13',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ two: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String27',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts
new file mode 100644
index 0000000000..b7c7572341
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts
@@ -0,0 +1,55 @@
+import type { Post } from '@prisma/client'
+
+import { posts, post, createPost, updatePost, deletePost } from './posts.js'
+import type { StandardScenario } from './posts.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('posts', () => {
+ scenario('returns all posts', async (scenario: StandardScenario) => {
+ const result = await posts()
+
+ expect(result.length).toEqual(Object.keys(scenario.post).length)
+ })
+
+ scenario('returns a single post', async (scenario: StandardScenario) => {
+ const result = await post({ id: scenario.post.one.id })
+
+ expect(result).toEqual(scenario.post.one)
+ })
+
+ scenario('creates a post', async (scenario: StandardScenario) => {
+ const result = await createPost({
+ input: {
+ title: 'String',
+ body: 'String',
+ authorId: scenario.post.two.authorId,
+ },
+ })
+
+ expect(result.title).toEqual('String')
+ expect(result.body).toEqual('String')
+ expect(result.authorId).toEqual(scenario.post.two.authorId)
+ })
+
+ scenario('updates a post', async (scenario: StandardScenario) => {
+ const original = (await post({ id: scenario.post.one.id })) as Post
+ const result = await updatePost({
+ id: original.id,
+ input: { title: 'String2' },
+ })
+
+ expect(result.title).toEqual('String2')
+ })
+
+ scenario('deletes a post', async (scenario: StandardScenario) => {
+ const original = (await deletePost({ id: scenario.post.one.id })) as Post
+ const result = await post({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts
new file mode 100644
index 0000000000..afbc87a734
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts
@@ -0,0 +1,42 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ PostRelationResolvers,
+} from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const posts: QueryResolvers['posts'] = () => {
+ return db.post.findMany()
+}
+
+export const post: QueryResolvers['post'] = ({ id }) => {
+ return db.post.findUnique({
+ where: { id },
+ })
+}
+
+export const createPost: MutationResolvers['createPost'] = ({ input }) => {
+ return db.post.create({
+ data: input,
+ })
+}
+
+export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
+ return db.post.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
+ return db.post.delete({
+ where: { id },
+ })
+}
+
+export const Post: PostRelationResolvers = {
+ author: (_obj, { root }) => {
+ return db.post.findUnique({ where: { id: root?.id } }).author()
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts
new file mode 100644
index 0000000000..390a0fef2d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts
@@ -0,0 +1,28 @@
+import type { Prisma, Produce } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ produce: {
+ one: {
+ data: {
+ name: 'String2231134',
+ quantity: 6978607,
+ price: 9949534,
+ region: 'String',
+ stall: { create: { name: 'String', stallNumber: 'String9443378' } },
+ },
+ },
+ two: {
+ data: {
+ name: 'String5325933',
+ quantity: 9629727,
+ price: 360315,
+ region: 'String',
+ stall: { create: { name: 'String', stallNumber: 'String8448512' } },
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts
new file mode 100644
index 0000000000..e58c263f08
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts
@@ -0,0 +1,67 @@
+import type { Produce } from '@prisma/client'
+
+import {
+ produces,
+ produce,
+ createProduce,
+ updateProduce,
+ deleteProduce,
+} from './produces.js'
+import type { StandardScenario } from './produces.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('produces', () => {
+ scenario('returns all produces', async (scenario: StandardScenario) => {
+ const result = await produces()
+
+ expect(result.length).toEqual(Object.keys(scenario.produce).length)
+ })
+
+ scenario('returns a single produce', async (scenario: StandardScenario) => {
+ const result = await produce({ id: scenario.produce.one.id })
+
+ expect(result).toEqual(scenario.produce.one)
+ })
+
+ scenario('creates a produce', async (scenario: StandardScenario) => {
+ const result = await createProduce({
+ input: {
+ name: 'String4747749',
+ quantity: 9239113,
+ price: 1579575,
+ region: 'String',
+ stallId: scenario.produce.two.stallId,
+ },
+ })
+
+ expect(result.name).toEqual('String4747749')
+ expect(result.quantity).toEqual(9239113)
+ expect(result.price).toEqual(1579575)
+ expect(result.region).toEqual('String')
+ expect(result.stallId).toEqual(scenario.produce.two.stallId)
+ })
+
+ scenario('updates a produce', async (scenario: StandardScenario) => {
+ const original = (await produce({ id: scenario.produce.one.id })) as Produce
+ const result = await updateProduce({
+ id: original.id,
+ input: { name: 'String9726252' },
+ })
+
+ expect(result.name).toEqual('String9726252')
+ })
+
+ scenario('deletes a produce', async (scenario: StandardScenario) => {
+ const original = (await deleteProduce({
+ id: scenario.produce.one.id,
+ })) as Produce
+ const result = await produce({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts
new file mode 100644
index 0000000000..40f84d949c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts
@@ -0,0 +1,47 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ ProduceRelationResolvers,
+} from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const produces: QueryResolvers['produces'] = () => {
+ return db.produce.findMany()
+}
+
+export const produce: QueryResolvers['produce'] = ({ id }) => {
+ return db.produce.findUnique({
+ where: { id },
+ })
+}
+
+export const createProduce: MutationResolvers['createProduce'] = ({
+ input,
+}) => {
+ return db.produce.create({
+ data: input,
+ })
+}
+
+export const updateProduce: MutationResolvers['updateProduce'] = ({
+ id,
+ input,
+}) => {
+ return db.produce.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteProduce: MutationResolvers['deleteProduce'] = ({ id }) => {
+ return db.produce.delete({
+ where: { id },
+ })
+}
+
+export const Produce: ProduceRelationResolvers = {
+ stall: (_obj, { root }) => {
+ return db.produce.findUnique({ where: { id: root?.id } }).stall()
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
new file mode 100644
index 0000000000..1e41eda101
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Stall } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ stall: {
+ one: { data: { name: 'String', stallNumber: 'String9136837' } },
+ two: { data: { name: 'String', stallNumber: 'String9074776' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts
new file mode 100644
index 0000000000..6956405eba
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts
@@ -0,0 +1,56 @@
+import type { Stall } from '@prisma/client'
+
+import {
+ stalls,
+ stall,
+ createStall,
+ updateStall,
+ deleteStall,
+} from './stalls.js'
+import type { StandardScenario } from './stalls.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('stalls', () => {
+ scenario('returns all stalls', async (scenario: StandardScenario) => {
+ const result = await stalls()
+
+ expect(result.length).toEqual(Object.keys(scenario.stall).length)
+ })
+
+ scenario('returns a single stall', async (scenario: StandardScenario) => {
+ const result = await stall({ id: scenario.stall.one.id })
+
+ expect(result).toEqual(scenario.stall.one)
+ })
+
+ scenario('creates a stall', async () => {
+ const result = await createStall({
+ input: { name: 'String', stallNumber: 'String1516174' },
+ })
+
+ expect(result.name).toEqual('String')
+ expect(result.stallNumber).toEqual('String1516174')
+ })
+
+ scenario('updates a stall', async (scenario: StandardScenario) => {
+ const original = (await stall({ id: scenario.stall.one.id })) as Stall
+ const result = await updateStall({
+ id: original.id,
+ input: { name: 'String2' },
+ })
+
+ expect(result.name).toEqual('String2')
+ })
+
+ scenario('deletes a stall', async (scenario: StandardScenario) => {
+ const original = (await deleteStall({ id: scenario.stall.one.id })) as Stall
+ const result = await stall({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts
new file mode 100644
index 0000000000..7d34a5eed2
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts
@@ -0,0 +1,45 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ StallRelationResolvers,
+} from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const stalls: QueryResolvers['stalls'] = () => {
+ return db.stall.findMany()
+}
+
+export const stall: QueryResolvers['stall'] = ({ id }) => {
+ return db.stall.findUnique({
+ where: { id },
+ })
+}
+
+export const createStall: MutationResolvers['createStall'] = ({ input }) => {
+ return db.stall.create({
+ data: input,
+ })
+}
+
+export const updateStall: MutationResolvers['updateStall'] = ({
+ id,
+ input,
+}) => {
+ return db.stall.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteStall: MutationResolvers['deleteStall'] = ({ id }) => {
+ return db.stall.delete({
+ where: { id },
+ })
+}
+
+export const Stall: StallRelationResolvers = {
+ produce: (_obj, { root }) => {
+ return db.stall.findUnique({ where: { id: root?.id } }).produce()
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts
new file mode 100644
index 0000000000..159e3cabd9
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts
@@ -0,0 +1,26 @@
+import type { Prisma, User } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ user: {
+ one: {
+ data: {
+ email: 'String9',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ two: {
+ data: {
+ email: 'String17',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts
new file mode 100644
index 0000000000..f754f23d1d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts
@@ -0,0 +1,10 @@
+import { user } from './users.js'
+import type { StandardScenario } from './users.scenarios.js'
+
+describe('users', () => {
+ scenario('returns a single user', async (scenario: StandardScenario) => {
+ const result = await user({ id: scenario.user.one.id })
+
+ expect(result).toEqual(scenario.user.one)
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts
new file mode 100644
index 0000000000..5aeb1f4e53
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts
@@ -0,0 +1,17 @@
+import type { QueryResolvers, UserRelationResolvers } from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export {}
+
+export const user: QueryResolvers['user'] = ({ id }) => {
+ return db.user.findUnique({
+ where: { id },
+ })
+}
+
+export const User: UserRelationResolvers = {
+ posts: (_obj, { root }) => {
+ return db.user.findUnique({ where: { id: root?.id } }).posts()
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/tsconfig.json b/__fixtures__/esm-fragment-test-project/api/tsconfig.json
new file mode 100644
index 0000000000..c9075d6fac
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "skipLibCheck": false,
+ "rootDirs": ["./src", "../.redwood/types/mirror/api/src"],
+ "paths": {
+ "src/*": ["./src/*", "../.redwood/types/mirror/api/src/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/api"]
+ },
+ "typeRoots": ["../node_modules/@types", "./node_modules/@types"],
+ "types": ["jest"],
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/api-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/api/vitest.config.ts b/__fixtures__/esm-fragment-test-project/api/vitest.config.ts
new file mode 100644
index 0000000000..f3de348f75
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/api/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config'
+
+import { cedarVitestPreset } from '@cedarjs/vite/api'
+
+export default defineConfig({
+ plugins: [cedarVitestPreset()],
+ test: {
+ globals: true,
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/graphql.config.cjs b/__fixtures__/esm-fragment-test-project/graphql.config.cjs
new file mode 100644
index 0000000000..d82bff34e0
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/graphql.config.cjs
@@ -0,0 +1,11 @@
+// This file is used by the VSCode GraphQL extension
+
+const { getPaths } = require('@cedarjs/project-config')
+
+/** @type {import('graphql-config').IGraphQLConfig} */
+const config = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
+}
+
+module.exports = config
diff --git a/__fixtures__/esm-fragment-test-project/package.json b/__fixtures__/esm-fragment-test-project/package.json
new file mode 100644
index 0000000000..e403c327ae
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/package.json
@@ -0,0 +1,32 @@
+{
+ "private": true,
+ "type": "module",
+ "workspaces": {
+ "packages": [
+ "api",
+ "web"
+ ]
+ },
+ "devDependencies": {
+ "@cedarjs/core": "0.0.5",
+ "@cedarjs/project-config": "0.0.5",
+ "@cedarjs/testing": "0.0.5",
+ "vitest": "3.2.4"
+ },
+ "eslintConfig": {
+ "extends": "@cedarjs/eslint-config",
+ "root": true
+ },
+ "engines": {
+ "node": "=20.x"
+ },
+ "prisma": {
+ "seed": "yarn rw exec seed"
+ },
+ "packageManager": "yarn@4.9.2",
+ "resolutions": {
+ "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
+ "react-is": "19.0.0-rc-f2df5694-20240916",
+ "vite": "5.4.16"
+ }
+}
diff --git a/__fixtures__/esm-fragment-test-project/prettier.config.cjs b/__fixtures__/esm-fragment-test-project/prettier.config.cjs
new file mode 100644
index 0000000000..9d81988854
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/prettier.config.cjs
@@ -0,0 +1,20 @@
+// https://prettier.io/docs/en/options.html
+/** @type {import('prettier').RequiredOptions} */
+module.exports = {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ overrides: [
+ {
+ files: 'Routes.*',
+ options: {
+ printWidth: 999,
+ },
+ },
+ ],
+ tailwindConfig: './web/config/tailwind.config.cjs',
+ plugins: ['prettier-plugin-tailwindcss'],
+}
diff --git a/__fixtures__/esm-fragment-test-project/redwood.toml b/__fixtures__/esm-fragment-test-project/redwood.toml
new file mode 100644
index 0000000000..42d996a73f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/redwood.toml
@@ -0,0 +1,24 @@
+# This file contains the configuration settings for your Redwood app.
+# This file is also what makes your Redwood app a Redwood app.
+# If you remove it and try to run `yarn rw dev`, you'll get an error.
+#
+# For the full list of options, see the "App Configuration: redwood.toml" doc:
+# https://redwoodjs.com/docs/app-configuration-redwood-toml
+
+[web]
+ title = "Cedar App"
+ port = "${WEB_DEV_PORT:8910}"
+ apiUrl = "/.redwood/functions"
+ includeEnvironmentVariables = [
+ # Add any ENV vars that should be available to the web side to this array
+ # See https://redwoodjs.com/docs/environment-variables#web
+ ]
+[api]
+ port = "${API_DEV_PORT:8911}"
+[browser]
+ open = true
+[notifications]
+ versionUpdates = ["latest"]
+
+[graphql]
+ fragments = true
diff --git a/__fixtures__/esm-fragment-test-project/scripts/.keep b/__fixtures__/esm-fragment-test-project/scripts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-fragment-test-project/scripts/seed.ts b/__fixtures__/esm-fragment-test-project/scripts/seed.ts
new file mode 100644
index 0000000000..ffdb815a6d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/scripts/seed.ts
@@ -0,0 +1,159 @@
+import { db } from 'api/src/lib/db.js'
+
+// Manually apply seeds via the `yarn rw prisma db seed` command.
+//
+// Seeds automatically run the first time you run the `yarn rw prisma migrate dev`
+// command and every time you run the `yarn rw prisma migrate reset` command.
+//
+// See https://redwoodjs.com/docs/database-seeds for more info
+
+export default async () => {
+ try {
+ const users = [
+ {
+ id: 1,
+ email: 'user.one@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User One',
+ salt: 'fake_salt',
+ },
+ {
+ id: 2,
+ email: 'user.two@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User Two',
+ salt: 'fake_salt',
+ },
+ ]
+
+ if ((await db.user.count()) === 0) {
+ await Promise.all(users.map((user) => db.user.create({ data: user })))
+ } else {
+ console.log('Users already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ const posts = [
+ {
+ title: 'Welcome to the blog!',
+ body: "I'm baby single- origin coffee kickstarter lo - fi paleo skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.",
+ authorId: 1,
+ },
+ {
+ title: 'A little more about me',
+ body: "Raclette shoreditch before they sold out lyft. Ethical bicycle rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage etsy pitchfork sriracha tumeric bushwick.",
+ authorId: 1,
+ },
+ {
+ title: 'What is the meaning of life?',
+ body: 'Meh waistcoat succulents umami asymmetrical, hoodie post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.',
+ authorId: 2,
+ },
+ ]
+
+ if ((await db.post.count()) === 0) {
+ await Promise.all(
+ posts.map(async (post) => {
+ const newPost = await db.post.create({ data: post })
+
+ console.log(newPost)
+ })
+ )
+ } else {
+ console.log('Posts already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ const stalls = [
+ {
+ id: 'clr0zv6ow000012nvo6r09vog',
+ name: 'Salad Veggies',
+ stallNumber: '1',
+ },
+ {
+ id: 'clr0zvne2000112nvyhzf1ifk',
+ name: 'Pie Veggies',
+ stallNumber: '2',
+ },
+ {
+ id: 'clr0zvne3000212nv6boae9qw',
+ name: 'Root Veggies',
+ stallNumber: '3',
+ },
+ ]
+
+ if ((await db.stall.count()) === 0) {
+ await Promise.all(
+ stalls.map(async (stall) => {
+ const newStall = await db.stall.create({ data: stall })
+
+ console.log(newStall)
+ })
+ )
+ } else {
+ console.log('Stalls already seeded')
+ }
+
+ const produce = [
+ {
+ id: 'clr0zwyoq000312nvfsu1efcw',
+ name: 'Lettuce',
+ quantity: 10,
+ price: 2,
+ ripenessIndicators: null,
+ region: '',
+ isSeedless: false,
+ vegetableFamily: 'Asteraceae',
+ stallId: 'clr0zv6ow000012nvo6r09vog',
+ },
+ {
+ id: 'clr0zy32x000412nvsya5g8q0',
+ name: 'Strawberries',
+ quantity: 24,
+ price: 3,
+ ripenessIndicators: 'Vitamin C',
+ region: 'California',
+ isSeedless: false,
+ vegetableFamily: 'Soft',
+ stallId: 'clr0zvne2000112nvyhzf1ifk',
+ },
+ ]
+
+ if ((await db.produce.count()) === 0) {
+ await Promise.all(
+ produce.map(async (produce) => {
+ const newProduce = await db.produce.create({ data: produce })
+
+ console.log(newProduce)
+ })
+ )
+ } else {
+ console.log('Produce already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ // Create your database records here! For example, seed some users:
+ //
+ // const users = [
+ // { name: 'Alice', email: 'alice@cedarjs.com' },
+ // { name: 'Bob', email: 'bob@cedarjs.com' },
+ // ]
+ //
+ // await db.user.createMany({ data: users })
+
+ console.info(
+ '\n No seed data, skipping. See scripts/seed.ts to start seeding your database!\n'
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json b/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json
new file mode 100644
index 0000000000..5690eec16c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "paths": {
+ "$api/*": ["../api/*"],
+ "api/*": ["../api/*"],
+ "$api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "$web/*": ["../web/*"],
+ "web/*": ["../web/*"],
+ "$web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "types/*": ["../types/*", "../web/types/*", "../api/types/*"]
+ },
+ "typeRoots": ["../node_modules/@types"],
+ "jsx": "preserve"
+ },
+ "include": [
+ ".",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/vitest.config.ts b/__fixtures__/esm-fragment-test-project/vitest.config.ts
new file mode 100644
index 0000000000..6c64e7920b
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: ['/{*,!(node_modules)/**/}/vite?(st).config.{js,ts}'],
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs b/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs
new file mode 100644
index 0000000000..f6d96e144e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs
@@ -0,0 +1,9 @@
+const path = require('path')
+
+module.exports = {
+ plugins: [
+ require('tailwindcss/nesting'),
+ require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.cjs')),
+ require('autoprefixer'),
+ ],
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs b/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs
new file mode 100644
index 0000000000..613a94fcab
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs
@@ -0,0 +1,10 @@
+const { join } = require('node:path')
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [join(__dirname, '../src/**/*.{js,jsx,ts,tsx}')],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/package.json b/__fixtures__/esm-fragment-test-project/web/package.json
new file mode 100644
index 0000000000..63050afa37
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "web",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "browserslist": {
+ "development": [
+ "last 1 version"
+ ],
+ "production": [
+ "defaults"
+ ]
+ },
+ "dependencies": {
+ "@cedarjs/auth-dbauth-web": "0.0.5",
+ "@cedarjs/forms": "0.0.5",
+ "@cedarjs/router": "0.0.5",
+ "@cedarjs/web": "0.0.5",
+ "humanize-string": "2.1.0",
+ "react": "19.0.0-rc-f2df5694-20240916",
+ "react-dom": "19.0.0-rc-f2df5694-20240916"
+ },
+ "devDependencies": {
+ "@cedarjs/vite": "0.0.5",
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.6",
+ "postcss-loader": "^8.1.1",
+ "tailwindcss": "^3.4.17"
+ }
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/public/README.md b/__fixtures__/esm-fragment-test-project/web/public/README.md
new file mode 100644
index 0000000000..1b09bf8361
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/public/README.md
@@ -0,0 +1,43 @@
+# Static Assets
+
+Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`.
+
+> Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes.
+
+### Example Use
+
+A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
+
+```
+
+```
+
+and
+
+```
+ alt="Logo" />
+```
+
+## Best Practices
+
+Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc.
+
+In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash.
+
+### Example Asset Import with Vite
+
+Instead of handling our logo image as a static file per the example above, we can do the following:
+
+```
+import React from "react"
+import logo from "./my-logo.jpg"
+
+
+function Header() {
+ return
+}
+
+export default Header
+```
+
+See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html)
diff --git a/__fixtures__/esm-fragment-test-project/web/public/favicon.png b/__fixtures__/esm-fragment-test-project/web/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+
+
+ {children}
+
+
+
+
+)
+
+export default App
diff --git a/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx b/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx
new file mode 100644
index 0000000000..d0ef9e47cf
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx
@@ -0,0 +1,18 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+
+
+
Redwood
+
+
+_by Tom Preston-Werner, Peter Pistorius, Rob Cameron, David Price, and more than
+250 amazing contributors (see end of file for a full list)._
+
+**Redwood is an opinionated, full-stack, JavaScript/TypeScript web application
+framework designed to keep you moving fast as your app grows from side project
+to startup.**
diff --git a/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx b/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx
new file mode 100644
index 0000000000..157454cad9
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx
@@ -0,0 +1,54 @@
+// In this file, all Page components from 'src/pages` are auto-imported. Nested
+// directories are supported, and should be uppercase. Each subdirectory will be
+// prepended onto the component name.
+//
+// Examples:
+//
+// 'src/pages/HomePage/HomePage.js' -> HomePage
+// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
+
+import { Router, Route, Private, Set } from '@cedarjs/router'
+
+import BlogLayout from 'src/layouts/BlogLayout'
+import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
+import HomePage from 'src/pages/HomePage'
+
+import { useAuth } from './auth.js'
+
+const Routes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Routes
diff --git a/__fixtures__/esm-fragment-test-project/web/src/auth.ts b/__fixtures__/esm-fragment-test-project/web/src/auth.ts
new file mode 100644
index 0000000000..ca02125c24
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/auth.ts
@@ -0,0 +1,5 @@
+import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/.keep b/__fixtures__/esm-fragment-test-project/web/src/components/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx
new file mode 100644
index 0000000000..29dbcf59fa
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx
@@ -0,0 +1,35 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import Author from './Author'
+
+const meta: Meta = {
+ component: Author,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+const author = {
+ email: 'story.user@email.com',
+ fullName: 'Story User',
+}
+
+export const Primary: Story = {
+ render: () => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx
new file mode 100644
index 0000000000..09e58d145e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx
@@ -0,0 +1,19 @@
+import { render } from '@cedarjs/testing/web'
+
+import Author from './Author'
+
+const author = {
+ email: 'test.user@email.com',
+ fullName: 'Test User',
+}
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('Author', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx
new file mode 100644
index 0000000000..f59f8b98f1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx
@@ -0,0 +1,16 @@
+interface Props {
+ author: {
+ email: string
+ fullName: string
+ }
+}
+
+const Author = ({ author }: Props) => {
+ return (
+
+ {author.fullName} ({author.email})
+
+ )
+}
+
+export default Author
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
new file mode 100644
index 0000000000..5dd8454b3d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
@@ -0,0 +1,9 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ author: {
+ __typename: 'User' as const,
+ id: 42,
+ email: 'fortytwo@42.com',
+ fullName: 'Forty Two',
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
new file mode 100644
index 0000000000..fd0a2b7337
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/AuthorCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
new file mode 100644
index 0000000000..4fbec70cb1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('AuthorCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
new file mode 100644
index 0000000000..95180b96f6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx
@@ -0,0 +1,42 @@
+import type {
+ FindAuthorQuery,
+ FindAuthorQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Author from 'src/components/Author'
+
+export const QUERY: TypedDocumentNode<
+ FindAuthorQuery,
+ FindAuthorQueryVariables
+> = gql`
+ query FindAuthorQuery($id: Int!) {
+ author: user(id: $id) {
+ email
+ fullName
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ author,
+}: CellSuccessProps) => (
+
+
+
+)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
new file mode 100644
index 0000000000..4c4daef53f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
@@ -0,0 +1,26 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogPost from './BlogPost'
+
+const meta: Meta = {
+ component: BlogPost,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
new file mode 100644
index 0000000000..c30451e8ad
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogPost from './BlogPost'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('BlogPost', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
new file mode 100644
index 0000000000..e13a2032e7
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx
@@ -0,0 +1,41 @@
+import { FindBlogPostQuery } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+
+import Author from 'src/components/Author'
+
+interface Props extends FindBlogPostQuery {}
+
+const BlogPost = ({ blogPost }: Props) => {
+ return (
+
+ {blogPost && (
+ <>
+
+
+ {new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(blogPost.createdAt))}{' '}
+ - By:
+
+
+
+ {blogPost.title}
+
+
+
+
+ {blogPost.body}
+
+ >
+ )}
+
+ )
+}
+
+export default BlogPost
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
new file mode 100644
index 0000000000..e805ec44db
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
@@ -0,0 +1,17 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPost: {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
new file mode 100644
index 0000000000..361d13a911
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/BlogPostCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
new file mode 100644
index 0000000000..9df5744f80
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('BlogPostCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
new file mode 100644
index 0000000000..d02ba269df
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
@@ -0,0 +1,46 @@
+import type {
+ FindBlogPostQuery,
+ FindBlogPostQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import BlogPost from 'src/components/BlogPost'
+
+export const QUERY: TypedDocumentNode<
+ FindBlogPostQuery,
+ FindBlogPostQueryVariables
+> = gql`
+ query FindBlogPostQuery($id: Int!) {
+ blogPost: post(id: $id) {
+ id
+ title
+ body
+ author {
+ email
+ fullName
+ }
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ blogPost,
+}: CellSuccessProps) => (
+
+)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
new file mode 100644
index 0000000000..b1569845f5
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
@@ -0,0 +1,47 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPosts: [
+ {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ {
+ __typename: 'Post' as const,
+ id: 43,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ {
+ __typename: 'Post' as const,
+ id: 44,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ ],
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
new file mode 100644
index 0000000000..bbd0cb8b0f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/BlogPostsCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
new file mode 100644
index 0000000000..5e7a947c9d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('BlogPostsCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
new file mode 100644
index 0000000000..3f77a396ed
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
@@ -0,0 +1,45 @@
+import type { BlogPostsQuery, BlogPostsQueryVariables } from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import BlogPost from 'src/components/BlogPost'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query BlogPostsQuery {
+ blogPosts: posts {
+ id
+ title
+ body
+ author {
+ email
+ fullName
+ }
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ blogPosts,
+}: CellSuccessProps) => (
+
+ {blogPosts.map((post) => (
+
+ ))}
+
+)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx
new file mode 100644
index 0000000000..8894a447b2
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx
@@ -0,0 +1,9 @@
+const Card = ({ children }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Card
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
new file mode 100644
index 0000000000..b6855a5b07
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
@@ -0,0 +1,26 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ClassWithClassField from './ClassWithClassField'
+
+const meta: Meta = {
+ component: ClassWithClassField,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
new file mode 100644
index 0000000000..0f342ba74b
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import ClassWithClassField from './ClassWithClassField'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('ClassWithClassField', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
new file mode 100644
index 0000000000..10cf41bad5
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
@@ -0,0 +1,12 @@
+class Bar {}
+
+class Foo {
+ // Without the correct babel plugins this will throw an error
+ public bar = new Bar()
+}
+
+const ClassWithClassField = () => {
+ return {new Foo().bar.toString()}
+}
+
+export default ClassWithClassField
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx
new file mode 100644
index 0000000000..f90bff1d17
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx
@@ -0,0 +1,98 @@
+import type {
+ DeleteContactMutation,
+ DeleteContactMutationVariables,
+ FindContactById,
+} from 'types/graphql.js'
+
+import { Link, routes, navigate } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { timeTag } from 'src/lib/formatters.js'
+
+const DELETE_CONTACT_MUTATION: TypedDocumentNode<
+ DeleteContactMutation,
+ DeleteContactMutationVariables
+> = gql`
+ mutation DeleteContactMutation($id: Int!) {
+ deleteContact(id: $id) {
+ id
+ }
+ }
+`
+
+interface Props {
+ contact: NonNullable
+}
+
+const Contact = ({ contact }: Props) => {
+ const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, {
+ onCompleted: () => {
+ toast.success('Contact deleted')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onDeleteClick = (id: DeleteContactMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete contact ' + id + '?')) {
+ deleteContact({ variables: { id } })
+ }
+ }
+
+ return (
+ <>
+
+
+
+ Contact {contact.id} Detail
+
+
+
+
+
+ Id
+ {contact.id}
+
+
+ Name
+ {contact.name}
+
+
+ Email
+ {contact.email}
+
+
+ Message
+ {contact.message}
+
+
+ Created at
+ {timeTag(contact.createdAt)}
+
+
+
+
+
+
+ Edit
+
+ onDeleteClick(contact.id)}
+ >
+ Delete
+
+
+ >
+ )
+}
+
+export default Contact
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
new file mode 100644
index 0000000000..c3470c7360
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
@@ -0,0 +1,43 @@
+import type {
+ FindContactById,
+ FindContactByIdVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Contact from 'src/components/Contact/Contact'
+
+export const QUERY: TypedDocumentNode<
+ FindContactById,
+ FindContactByIdVariables
+> = gql`
+ query FindContactById($id: Int!) {
+ contact: contact(id: $id) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Contact not found
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ contact,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
new file mode 100644
index 0000000000..024b762b2d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
@@ -0,0 +1,101 @@
+import type { EditContactById, UpdateContactInput } from 'types/graphql.js'
+
+import type { RWGqlError } from '@cedarjs/forms'
+import {
+ Form,
+ FormError,
+ FieldError,
+ Label,
+ TextField,
+ Submit,
+} from '@cedarjs/forms'
+
+type FormContact = NonNullable
+
+interface ContactFormProps {
+ contact?: EditContactById['contact']
+ onSave: (data: UpdateContactInput, id?: FormContact['id']) => void
+ error: RWGqlError
+ loading: boolean
+}
+
+const ContactForm = (props: ContactFormProps) => {
+ const onSubmit = (data: FormContact) => {
+ props.onSave(data, props?.contact?.id)
+ }
+
+ return (
+
+ )
+}
+
+export default ContactForm
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx
new file mode 100644
index 0000000000..30528f3f3f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx
@@ -0,0 +1,102 @@
+import type {
+ DeleteContactMutation,
+ DeleteContactMutationVariables,
+ FindContacts,
+} from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { QUERY } from 'src/components/Contact/ContactsCell'
+import { timeTag, truncate } from 'src/lib/formatters.js'
+
+const DELETE_CONTACT_MUTATION: TypedDocumentNode<
+ DeleteContactMutation,
+ DeleteContactMutationVariables
+> = gql`
+ mutation DeleteContactMutation($id: Int!) {
+ deleteContact(id: $id) {
+ id
+ }
+ }
+`
+
+const ContactsList = ({ contacts }: FindContacts) => {
+ const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, {
+ onCompleted: () => {
+ toast.success('Contact deleted')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ // This refetches the query on the list page. Read more about other ways to
+ // update the cache over here:
+ // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
+ refetchQueries: [{ query: QUERY }],
+ awaitRefetchQueries: true,
+ })
+
+ const onDeleteClick = (id: DeleteContactMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete contact ' + id + '?')) {
+ deleteContact({ variables: { id } })
+ }
+ }
+
+ return (
+
+
+
+
+ Id
+ Name
+ Email
+ Message
+ Created at
+
+
+
+
+ {contacts.map((contact) => (
+
+ {truncate(contact.id)}
+ {truncate(contact.name)}
+ {truncate(contact.email)}
+ {truncate(contact.message)}
+ {timeTag(contact.createdAt)}
+
+
+
+ Show
+
+
+ Edit
+
+ onDeleteClick(contact.id)}
+ >
+ Delete
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export default ContactsList
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
new file mode 100644
index 0000000000..e64d6a8e96
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
@@ -0,0 +1,46 @@
+import type { FindContacts, FindContactsVariables } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Contacts from 'src/components/Contact/Contacts'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query FindContacts {
+ contacts {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => {
+ return (
+
+ No contacts yet.{' '}
+
+ Create one?
+
+
+ )
+}
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ contacts,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
new file mode 100644
index 0000000000..622e63f686
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
@@ -0,0 +1,89 @@
+import type {
+ EditContactById,
+ UpdateContactInput,
+ UpdateContactMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+import { useMutation } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import ContactForm from 'src/components/Contact/ContactForm'
+
+export const QUERY: TypedDocumentNode = gql`
+ query EditContactById($id: Int!) {
+ contact: contact(id: $id) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+const UPDATE_CONTACT_MUTATION: TypedDocumentNode<
+ EditContactById,
+ UpdateContactMutationVariables
+> = gql`
+ mutation UpdateContactMutation($id: Int!, $input: UpdateContactInput!) {
+ updateContact(id: $id, input: $input) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({ contact }: CellSuccessProps) => {
+ const [updateContact, { loading, error }] = useMutation(
+ UPDATE_CONTACT_MUTATION,
+ {
+ onCompleted: () => {
+ toast.success('Contact updated')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+
+ const onSave = (
+ input: UpdateContactInput,
+ id: EditContactById['contact']['id']
+ ) => {
+ updateContact({ variables: { id, input } })
+ }
+
+ return (
+
+
+
+ Edit Contact {contact?.id}
+
+
+
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx
new file mode 100644
index 0000000000..2cb66a3c30
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx
@@ -0,0 +1,55 @@
+import type {
+ CreateContactMutation,
+ CreateContactInput,
+ CreateContactMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import ContactForm from 'src/components/Contact/ContactForm'
+
+const CREATE_CONTACT_MUTATION: TypedDocumentNode<
+ CreateContactMutation,
+ CreateContactMutationVariables
+> = gql`
+ mutation CreateContactMutation($input: CreateContactInput!) {
+ createContact(input: $input) {
+ id
+ }
+ }
+`
+
+const NewContact = () => {
+ const [createContact, { loading, error }] = useMutation(
+ CREATE_CONTACT_MUTATION,
+ {
+ onCompleted: () => {
+ toast.success('Contact created')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+
+ const onSave = (input: CreateContactInput) => {
+ createContact({ variables: { input } })
+ }
+
+ return (
+
+ )
+}
+
+export default NewContact
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx
new file mode 100644
index 0000000000..54fb6f98b0
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx
@@ -0,0 +1,35 @@
+import type { Fruit } from 'types/graphql.js'
+
+import { registerFragment } from '@cedarjs/web/apollo'
+
+import Card from 'src/components/Card'
+import StallInfo from 'src/components/StallInfo'
+
+const { useRegisteredFragment } = registerFragment(gql`
+ fragment Fruit_info on Fruit {
+ id
+ name
+ isSeedless
+ ripenessIndicators
+ stall {
+ ...Stall_info
+ }
+ }
+`)
+
+const FruitInfo = ({ id }: { id: string }) => {
+ const { data: fruit, complete } = useRegisteredFragment(id)
+
+ return (
+ complete && (
+
+ Fruit Name: {fruit.name}
+ Seeds? {fruit.isSeedless ? 'Yes' : 'No'}
+ Ripeness: {fruit.ripenessIndicators}
+
+
+ )
+ )
+}
+
+export default FruitInfo
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
new file mode 100644
index 0000000000..53a0bd1079
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
@@ -0,0 +1,78 @@
+import type {
+ EditPostById,
+ UpdatePostInput,
+ UpdatePostMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+import { useMutation } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import PostForm from 'src/components/Post/PostForm'
+
+export const QUERY: TypedDocumentNode = gql`
+ query EditPostById($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+const UPDATE_POST_MUTATION: TypedDocumentNode<
+ EditPostById,
+ UpdatePostMutationVariables
+> = gql`
+ mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
+ updatePost(id: $id, input: $input) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({ post }: CellSuccessProps) => {
+ const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post updated')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSave = (input: UpdatePostInput, id: EditPostById['post']['id']) => {
+ updatePost({ variables: { id, input } })
+ }
+
+ return (
+
+
+
+ Edit Post {post?.id}
+
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx
new file mode 100644
index 0000000000..4a3a8b91bd
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx
@@ -0,0 +1,52 @@
+import type {
+ CreatePostMutation,
+ CreatePostInput,
+ CreatePostMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import PostForm from 'src/components/Post/PostForm'
+
+const CREATE_POST_MUTATION: TypedDocumentNode<
+ CreatePostMutation,
+ CreatePostMutationVariables
+> = gql`
+ mutation CreatePostMutation($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ }
+ }
+`
+
+const NewPost = () => {
+ const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post created')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSave = (input: CreatePostInput) => {
+ createPost({ variables: { input } })
+ }
+
+ return (
+
+ )
+}
+
+export default NewPost
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx
new file mode 100644
index 0000000000..dfbc5a413c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx
@@ -0,0 +1,98 @@
+import type {
+ DeletePostMutation,
+ DeletePostMutationVariables,
+ FindPostById,
+} from 'types/graphql.js'
+
+import { Link, routes, navigate } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { timeTag } from 'src/lib/formatters.js'
+
+const DELETE_POST_MUTATION: TypedDocumentNode<
+ DeletePostMutation,
+ DeletePostMutationVariables
+> = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+interface Props {
+ post: NonNullable
+}
+
+const Post = ({ post }: Props) => {
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post deleted')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id } })
+ }
+ }
+
+ return (
+ <>
+
+
+
+ Post {post.id} Detail
+
+
+
+
+
+ Id
+ {post.id}
+
+
+ Title
+ {post.title}
+
+
+ Body
+ {post.body}
+
+
+ Author id
+ {post.authorId}
+
+
+ Created at
+ {timeTag(post.createdAt)}
+
+
+
+
+
+
+ Edit
+
+ onDeleteClick(post.id)}
+ >
+ Delete
+
+
+ >
+ )
+}
+
+export default Post
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx
new file mode 100644
index 0000000000..f156749b0d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx
@@ -0,0 +1,36 @@
+import type { FindPostById, FindPostByIdVariables } from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Post from 'src/components/Post/Post'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query FindPostById($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => Post not found
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ post,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx
new file mode 100644
index 0000000000..4a17df0ca1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx
@@ -0,0 +1,102 @@
+import type { EditPostById, UpdatePostInput } from 'types/graphql.js'
+
+import type { RWGqlError } from '@cedarjs/forms'
+import {
+ Form,
+ FormError,
+ FieldError,
+ Label,
+ TextField,
+ NumberField,
+ Submit,
+} from '@cedarjs/forms'
+
+type FormPost = NonNullable
+
+interface PostFormProps {
+ post?: EditPostById['post']
+ onSave: (data: UpdatePostInput, id?: FormPost['id']) => void
+ error: RWGqlError
+ loading: boolean
+}
+
+const PostForm = (props: PostFormProps) => {
+ const onSubmit = (data: FormPost) => {
+ props.onSave(data, props?.post?.id)
+ }
+
+ return (
+
+ )
+}
+
+export default PostForm
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx
new file mode 100644
index 0000000000..edbf98fe98
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx
@@ -0,0 +1,102 @@
+import type {
+ DeletePostMutation,
+ DeletePostMutationVariables,
+ FindPosts,
+} from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { QUERY } from 'src/components/Post/PostsCell'
+import { timeTag, truncate } from 'src/lib/formatters.js'
+
+const DELETE_POST_MUTATION: TypedDocumentNode<
+ DeletePostMutation,
+ DeletePostMutationVariables
+> = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+const PostsList = ({ posts }: FindPosts) => {
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post deleted')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ // This refetches the query on the list page. Read more about other ways to
+ // update the cache over here:
+ // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
+ refetchQueries: [{ query: QUERY }],
+ awaitRefetchQueries: true,
+ })
+
+ const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id } })
+ }
+ }
+
+ return (
+
+
+
+
+ Id
+ Title
+ Body
+ Author id
+ Created at
+
+
+
+
+ {posts.map((post) => (
+
+ {truncate(post.id)}
+ {truncate(post.title)}
+ {truncate(post.body)}
+ {truncate(post.authorId)}
+ {timeTag(post.createdAt)}
+
+
+
+ Show
+
+
+ Edit
+
+ onDeleteClick(post.id)}
+ >
+ Delete
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export default PostsList
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
new file mode 100644
index 0000000000..8a3f465701
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
@@ -0,0 +1,45 @@
+import type { FindPosts, FindPostsVariables } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Posts from 'src/components/Post/Posts'
+
+export const QUERY: TypedDocumentNode = gql`
+ query FindPosts {
+ posts {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => {
+ return (
+
+ No posts yet.{' '}
+
+ Create one?
+
+
+ )
+}
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ posts,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx
new file mode 100644
index 0000000000..c70c97cd0f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx
@@ -0,0 +1,26 @@
+import type { Produce } from 'types/graphql.js'
+
+import { registerFragment } from '@cedarjs/web/apollo'
+
+import Card from 'src/components/Card'
+
+const { useRegisteredFragment } = registerFragment(gql`
+ fragment Produce_info on Produce {
+ id
+ name
+ }
+`)
+
+const ProduceInfo = ({ id }: { id: string }) => {
+ const { data, complete } = useRegisteredFragment(id)
+
+ return (
+ complete && (
+
+ Produce Name: {data.name}
+
+ )
+ )
+}
+
+export default ProduceInfo
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx
new file mode 100644
index 0000000000..b828da4b1c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx
@@ -0,0 +1,24 @@
+import type { Stall } from 'types/graphql.js'
+
+import { registerFragment } from '@cedarjs/web/apollo'
+
+const { useRegisteredFragment } = registerFragment(gql`
+ fragment Stall_info on Stall {
+ id
+ name
+ }
+`)
+
+const StallInfo = ({ id }: { id: string }) => {
+ const { data, complete } = useRegisteredFragment(id)
+
+ return (
+ complete && (
+
+
Stall Name: {data.name}
+
+ )
+ )
+}
+
+export default StallInfo
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx
new file mode 100644
index 0000000000..71e53a04f6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx
@@ -0,0 +1,35 @@
+import type { Vegetable } from 'types/graphql.js'
+
+import { registerFragment } from '@cedarjs/web/apollo'
+
+import Card from 'src/components/Card'
+import StallInfo from 'src/components/StallInfo'
+
+const { useRegisteredFragment } = registerFragment(gql`
+ fragment Vegetable_info on Vegetable {
+ id
+ name
+ vegetableFamily
+ isPickled
+ stall {
+ ...Stall_info
+ }
+ }
+`)
+
+const VegetableInfo = ({ id }: { id: string }) => {
+ const { data: vegetable, complete } = useRegisteredFragment(id)
+
+ return (
+ complete && (
+
+ Vegetable Name: {vegetable.name}
+ Pickled? {vegetable.isPickled ? 'Yes' : 'No'}
+ Family: {vegetable.vegetableFamily}
+
+
+ )
+ )
+}
+
+export default VegetableInfo
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
new file mode 100644
index 0000000000..cf02b60610
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
@@ -0,0 +1,17 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ waterfallBlogPost: {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 7,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'se7en@7.com',
+ fullName: 'Se7en Lastname',
+ },
+ },
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
new file mode 100644
index 0000000000..7109babeb3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell'
+import { standard } from './WaterfallBlogPostCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/WaterfallBlogPostCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
new file mode 100644
index 0000000000..6701312f2c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
@@ -0,0 +1,44 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell'
+import { standard } from './WaterfallBlogPostCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('WaterfallBlogPostCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render(
+
+ )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
new file mode 100644
index 0000000000..075ca427a6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
@@ -0,0 +1,67 @@
+import type {
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import AuthorCell from 'src/components/AuthorCell'
+
+export const QUERY: TypedDocumentNode<
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables
+> = gql`
+ query FindWaterfallBlogPostQuery($id: Int!) {
+ waterfallBlogPost: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ waterfallBlogPost,
+}: CellSuccessProps<
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables
+>) => (
+
+ {waterfallBlogPost && (
+ <>
+
+
+ {new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(waterfallBlogPost.createdAt))}{' '}
+ - By:
+
+
+ {waterfallBlogPost.title}
+
+
+
+ {waterfallBlogPost.body}
+
+ >
+ )}
+
+)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx b/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx
new file mode 100644
index 0000000000..915c14d76d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx
@@ -0,0 +1,35 @@
+import { hydrateRoot, createRoot } from 'react-dom/client'
+
+import App from './App'
+import Routes from './Routes'
+
+/**
+ * When `#redwood-app` isn't empty then it's very likely that you're using
+ * prerendering. So React attaches event listeners to the existing markup
+ * rather than replacing it.
+ * https://react.dev/reference/react-dom/client/hydrateRoot
+ */
+const redwoodAppElement = document.getElementById('redwood-app')
+
+if (!redwoodAppElement) {
+ throw new Error(
+ "Could not find an element with ID 'redwood-app'. Please ensure it " +
+ "exists in your 'web/src/index.html' file."
+ )
+}
+
+if (redwoodAppElement.children?.length > 0) {
+ hydrateRoot(
+ redwoodAppElement,
+
+
+
+ )
+} else {
+ const root = createRoot(redwoodAppElement)
+ root.render(
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts b/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts
new file mode 100644
index 0000000000..889b16ceeb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts
@@ -0,0 +1,12 @@
+export interface PossibleTypesResultData {
+ possibleTypes: {
+ [key: string]: string[]
+ }
+}
+const result: PossibleTypesResultData = {
+ possibleTypes: {
+ Groceries: ['Fruit', 'Vegetable'],
+ Grocery: ['Fruit', 'Vegetable'],
+ },
+}
+export default result
diff --git a/__fixtures__/esm-fragment-test-project/web/src/index.css b/__fixtures__/esm-fragment-test-project/web/src/index.css
new file mode 100644
index 0000000000..b31cb3378f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/index.css
@@ -0,0 +1,13 @@
+/**
+ * START --- SETUP TAILWINDCSS EDIT
+ *
+ * `yarn rw setup ui tailwindcss` placed these directives here
+ * to inject Tailwind's styles into your CSS.
+ * For more information, see: https://tailwindcss.com/docs/installation
+ */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+/**
+ * END --- SETUP TAILWINDCSS EDIT
+ */
diff --git a/__fixtures__/esm-fragment-test-project/web/src/index.html b/__fixtures__/esm-fragment-test-project/web/src/index.html
new file mode 100644
index 0000000000..e240b8eb8c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/.keep b/__fixtures__/esm-fragment-test-project/web/src/layouts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
new file mode 100644
index 0000000000..43f3b5d106
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogLayout from './BlogLayout'
+
+const meta: Meta = {
+ component: BlogLayout,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
new file mode 100644
index 0000000000..60ff1b9368
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogLayout from './BlogLayout'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('BlogLayout', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
new file mode 100644
index 0000000000..87af741fa0
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
@@ -0,0 +1,85 @@
+type BlogLayoutProps = {
+ children?: React.ReactNode
+}
+
+import { Link, NavLink, routes } from '@cedarjs/router'
+
+import { useAuth } from 'src/auth'
+
+const BlogLayout = ({ children }: BlogLayoutProps) => {
+ const { logOut, isAuthenticated } = useAuth()
+
+ return (
+ <>
+
+
+
+ Cedar Blog
+
+
+
+
+
+
+ About
+
+
+
+
+ Contact Us
+
+
+
+
+ Admin
+
+
+ {isAuthenticated && (
+
+
+ Log Out
+
+
+ )}
+ {!isAuthenticated && (
+
+
+ Log In
+
+
+ )}
+
+
+
+
+ {children}
+
+ >
+ )
+}
+
+export default BlogLayout
diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
new file mode 100644
index 0000000000..626f0d5cc5
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
@@ -0,0 +1,37 @@
+import { Link, routes } from '@cedarjs/router'
+import { Toaster } from '@cedarjs/web/toast'
+
+type LayoutProps = {
+ title: string
+ titleTo: keyof typeof routes
+ buttonLabel: string
+ buttonTo: keyof typeof routes
+ children: React.ReactNode
+}
+
+const ScaffoldLayout = ({
+ title,
+ titleTo,
+ buttonLabel,
+ buttonTo,
+ children,
+}: LayoutProps) => {
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ +
{buttonLabel}
+
+
+
{children}
+
+ )
+}
+
+export default ScaffoldLayout
diff --git a/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx
new file mode 100644
index 0000000000..0383c42716
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx
@@ -0,0 +1,192 @@
+import { render, waitFor, screen } from '@cedarjs/testing/web'
+
+import {
+ formatEnum,
+ jsonTruncate,
+ truncate,
+ timeTag,
+ jsonDisplay,
+ checkboxInputTag,
+} from './formatters.js'
+
+describe('formatEnum', () => {
+ it('handles nullish values', () => {
+ expect(formatEnum(null)).toEqual('')
+ expect(formatEnum('')).toEqual('')
+ expect(formatEnum(undefined)).toEqual('')
+ })
+
+ it('formats a list of values', () => {
+ expect(
+ formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET'])
+ ).toEqual('Red, Orange, Yellow, Green, Blue, Violet')
+ })
+
+ it('formats a single value', () => {
+ expect(formatEnum('DARK_BLUE')).toEqual('Dark blue')
+ })
+
+ it('returns an empty string for values of the wrong type (for JS projects)', () => {
+ // @ts-expect-error - Testing JS scenario
+ expect(formatEnum(5)).toEqual('')
+ })
+})
+
+describe('truncate', () => {
+ it('truncates really long strings', () => {
+ expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000)
+ expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/)
+ })
+
+ it('does not modify short strings', () => {
+ expect(truncate('Short strinG')).toEqual('Short strinG')
+ })
+
+ it('adds ... to the end of truncated strings', () => {
+ expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/)
+ })
+
+ it('accepts numbers', () => {
+ expect(truncate(123)).toEqual('123')
+ expect(truncate(0)).toEqual('0')
+ expect(truncate(0o000)).toEqual('0')
+ })
+
+ it('handles arguments of invalid type', () => {
+ // @ts-expect-error - Testing JS scenario
+ expect(truncate(false)).toEqual('false')
+
+ expect(truncate(undefined)).toEqual('')
+ expect(truncate(null)).toEqual('')
+ })
+})
+
+describe('jsonTruncate', () => {
+ it('truncates large json structures', () => {
+ expect(
+ jsonTruncate({
+ foo: 'foo',
+ bar: 'bar',
+ baz: 'baz',
+ kittens: 'kittens meow',
+ bazinga: 'Sheldon',
+ nested: {
+ foobar: 'I have no imagination',
+ two: 'Second nested item',
+ },
+ five: 5,
+ bool: false,
+ })
+ ).toMatch(/.+\n.+\w\.\.\.$/s)
+ })
+})
+
+describe('timeTag', () => {
+ it('renders a date', async () => {
+ render({timeTag(new Date('1970-08-20').toUTCString())}
)
+
+ await waitFor(() => screen.getByText(/1970.*00:00:00/))
+ })
+
+ it('can take an empty input string', async () => {
+ expect(timeTag('')).toEqual('')
+ })
+})
+
+describe('jsonDisplay', () => {
+ it('produces the correct output', () => {
+ expect(
+ jsonDisplay({
+ title: 'TOML Example (but in JSON)',
+ database: {
+ data: [['delta', 'phi'], [3.14]],
+ enabled: true,
+ ports: [8000, 8001, 8002],
+ temp_targets: {
+ case: 72.0,
+ cpu: 79.5,
+ },
+ },
+ owner: {
+ dob: '1979-05-27T07:32:00-08:00',
+ name: 'Tom Preston-Werner',
+ },
+ servers: {
+ alpha: {
+ ip: '10.0.0.1',
+ role: 'frontend',
+ },
+ beta: {
+ ip: '10.0.0.2',
+ role: 'backend',
+ },
+ },
+ })
+ ).toMatchInlineSnapshot(`
+
+
+ {
+ "title": "TOML Example (but in JSON)",
+ "database": {
+ "data": [
+ [
+ "delta",
+ "phi"
+ ],
+ [
+ 3.14
+ ]
+ ],
+ "enabled": true,
+ "ports": [
+ 8000,
+ 8001,
+ 8002
+ ],
+ "temp_targets": {
+ "case": 72,
+ "cpu": 79.5
+ }
+ },
+ "owner": {
+ "dob": "1979-05-27T07:32:00-08:00",
+ "name": "Tom Preston-Werner"
+ },
+ "servers": {
+ "alpha": {
+ "ip": "10.0.0.1",
+ "role": "frontend"
+ },
+ "beta": {
+ "ip": "10.0.0.2",
+ "role": "backend"
+ }
+ }
+ }
+
+
+ `)
+ })
+})
+
+describe('checkboxInputTag', () => {
+ it('can be checked', () => {
+ render(checkboxInputTag(true))
+ expect(screen.getByRole('checkbox')).toBeChecked()
+ })
+
+ it('can be unchecked', () => {
+ render(checkboxInputTag(false))
+ expect(screen.getByRole('checkbox')).not.toBeChecked()
+ })
+
+ it('is disabled when checked', () => {
+ render(checkboxInputTag(true))
+ expect(screen.getByRole('checkbox')).toBeDisabled()
+ })
+
+ it('is disabled when unchecked', () => {
+ render(checkboxInputTag(false))
+ expect(screen.getByRole('checkbox')).toBeDisabled()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx
new file mode 100644
index 0000000000..8ab9e806e3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+
+import humanize from 'humanize-string'
+
+const MAX_STRING_LENGTH = 150
+
+export const formatEnum = (values: string | string[] | null | undefined) => {
+ let output = ''
+
+ if (Array.isArray(values)) {
+ const humanizedValues = values.map((value) => humanize(value))
+ output = humanizedValues.join(', ')
+ } else if (typeof values === 'string') {
+ output = humanize(values)
+ }
+
+ return output
+}
+
+export const jsonDisplay = (obj: unknown) => {
+ return (
+
+ {JSON.stringify(obj, null, 2)}
+
+ )
+}
+
+export const truncate = (value: string | number) => {
+ let output = value?.toString() ?? ''
+
+ if (output.length > MAX_STRING_LENGTH) {
+ output = output.substring(0, MAX_STRING_LENGTH) + '...'
+ }
+
+ return output
+}
+
+export const jsonTruncate = (obj: unknown) => {
+ return truncate(JSON.stringify(obj, null, 2))
+}
+
+export const timeTag = (dateTime?: string) => {
+ let output: string | JSX.Element = ''
+
+ if (dateTime) {
+ output = (
+
+ {new Date(dateTime).toUTCString()}
+
+ )
+ }
+
+ return output
+}
+
+export const checkboxInputTag = (checked: boolean) => {
+ return
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
new file mode 100644
index 0000000000..b8259100eb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import AboutPage from './AboutPage'
+
+const meta: Meta = {
+ component: AboutPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
new file mode 100644
index 0000000000..13f28830eb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import AboutPage from './AboutPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('AboutPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx
new file mode 100644
index 0000000000..b0476d6e22
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx
@@ -0,0 +1,10 @@
+const AboutPage = () => {
+ return (
+
+ This site was created to demonstrate my mastery of Redwood: Look on my
+ works, ye mighty, and despair!
+
+ )
+}
+
+export default AboutPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
new file mode 100644
index 0000000000..e29f84e457
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
@@ -0,0 +1,5 @@
+import { db } from '$api/src/lib/db.js'
+
+export async function routeParameters() {
+ return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id }))
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
new file mode 100644
index 0000000000..b8abecc304
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogPostPage from './BlogPostPage'
+
+const meta: Meta = {
+ component: BlogPostPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
new file mode 100644
index 0000000000..1e1e4be5fb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogPostPage from './BlogPostPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('BlogPostPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
new file mode 100644
index 0000000000..52aac7a3d6
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
@@ -0,0 +1,20 @@
+// import { Link, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+
+type BlogPostPageProps = {
+ id: number
+}
+
+import BlogPostCell from 'src/components/BlogPostCell'
+
+const BlogPostPage = ({ id }: BlogPostPageProps) => {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+export default BlogPostPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
new file mode 100644
index 0000000000..9af63b0a3d
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
@@ -0,0 +1,11 @@
+import ContactCell from 'src/components/Contact/ContactCell'
+
+type ContactPageProps = {
+ id: number
+}
+
+const ContactPage = ({ id }: ContactPageProps) => {
+ return
+}
+
+export default ContactPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
new file mode 100644
index 0000000000..7bc4048094
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
@@ -0,0 +1,7 @@
+import ContactsCell from 'src/components/Contact/ContactsCell'
+
+const ContactsPage = () => {
+ return
+}
+
+export default ContactsPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
new file mode 100644
index 0000000000..7241f71f7f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
@@ -0,0 +1,11 @@
+import EditContactCell from 'src/components/Contact/EditContactCell'
+
+type ContactPageProps = {
+ id: number
+}
+
+const EditContactPage = ({ id }: ContactPageProps) => {
+ return
+}
+
+export default EditContactPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
new file mode 100644
index 0000000000..2d4cc9274e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
@@ -0,0 +1,7 @@
+import NewContact from 'src/components/Contact/NewContact'
+
+const NewContactPage = () => {
+ return
+}
+
+export default NewContactPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
new file mode 100644
index 0000000000..80eb779856
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ContactUsPage from './ContactUsPage'
+
+const meta: Meta = {
+ component: ContactUsPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
new file mode 100644
index 0000000000..0731835b58
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import ContactUsPage from './ContactUsPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('ContactUsPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
new file mode 100644
index 0000000000..de6488e1a7
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
@@ -0,0 +1,133 @@
+import { useState } from 'react'
+
+import { useForm } from 'react-hook-form'
+
+import {
+ Form,
+ TextField,
+ TextAreaField,
+ Submit,
+ FieldError,
+ Label,
+} from '@cedarjs/forms'
+import { useBlocker } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+const CREATE_CONTACT = gql`
+ mutation CreateContactMutation($input: CreateContactInput!) {
+ createContact(input: $input) {
+ id
+ }
+ }
+`
+
+const ContactUsPage = () => {
+ const formMethods = useForm()
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const blocker = useBlocker({
+ when: formMethods.formState.isDirty && !isSubmitting,
+ })
+
+ const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
+ onCompleted: () => {
+ toast.success('Thank you for your submission!')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSubmit = async (data) => {
+ setIsSubmitting(true)
+ try {
+ await create({ variables: { input: data } })
+ formMethods.reset(data)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default ContactUsPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
new file mode 100644
index 0000000000..adb222dbce
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import DoublePage from './DoublePage'
+
+const meta: Meta = {
+ component: DoublePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
new file mode 100644
index 0000000000..b308591403
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import DoublePage from './DoublePage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('DoublePage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx
new file mode 100644
index 0000000000..75aef1171e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx
@@ -0,0 +1,39 @@
+import { Metadata } from '@cedarjs/web'
+
+import test from './test.png'
+
+const DoublePage = () => {
+ return (
+ <>
+
+
+ DoublePage
+
+ This page exists to make sure we don't regress on{' '}
+
+ #7757
+
+
+ For RW#7757 it needs to be a page that is not wrapped in a Set
+
+ We also use this page to make sure we don't regress on{' '}
+
+ #317
+
+
+
+ >
+ )
+}
+
+export default DoublePage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+
+
+ Something went wrong
+
+
+
+ ))
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
new file mode 100644
index 0000000000..6f7236d1d0
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -0,0 +1,94 @@
+import { useEffect, useRef } from 'react'
+
+import { Form, Label, TextField, Submit, FieldError } from '@cedarjs/forms'
+import { navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const ForgotPasswordPage = () => {
+ const { isAuthenticated, forgotPassword } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef?.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: { username: string }) => {
+ const response = await forgotPassword(data.username)
+
+ if (response.error) {
+ toast.error(response.error)
+ } else {
+ // The function `forgotPassword.handler` in api/src/functions/auth.js has
+ // been invoked, let the user know how to get the link to reset their
+ // password (sent in email, perhaps?)
+ toast.success(
+ 'A link to reset your password was sent to ' + response.email
+ )
+ navigate(routes.login())
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default ForgotPasswordPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.stories.tsx
new file mode 100644
index 0000000000..86979d77f1
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import GroceriesPage from './GroceriesPage'
+
+const meta: Meta = {
+ component: GroceriesPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.test.tsx
new file mode 100644
index 0000000000..8dddc46b4f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import GroceriesPage from './GroceriesPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('GroceriesPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.tsx
new file mode 100644
index 0000000000..77e5869673
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/GroceriesPage/GroceriesPage.tsx
@@ -0,0 +1,55 @@
+// import { Link, routes } from '@cedarjs/router'
+import { Metadata, useQuery } from '@cedarjs/web';
+
+import FruitInfo from "src/components/FruitInfo";
+import ProduceInfo from "src/components/ProduceInfo";
+import VegetableInfo from "src/components/VegetableInfo";
+
+const GET_GROCERIES = gql`
+ query GetGroceries {
+ groceries {
+ ...Fruit_info
+ ...Vegetable_info
+ }
+ }
+`;
+
+const GET_PRODUCE = gql`
+ query GetProduce {
+ produces {
+ ...Produce_info
+ }
+ }
+`;
+
+const GroceriesPage = () => {
+ const { data: groceryData, loading: groceryLoading } =
+ useQuery(GET_GROCERIES)
+ const { data: produceData, loading: produceLoading } =
+ useQuery(GET_PRODUCE)
+
+ return (
+
+
+
+
+ {!groceryLoading &&
+ groceryData.groceries.map((fruit) => (
+
+ ))}
+
+ {!groceryLoading &&
+ groceryData.groceries.map((vegetable) => (
+
+ ))}
+
+ {!produceLoading &&
+ produceData.produces?.map((produce) => (
+
+ ))}
+
+
+ )
+}
+
+export default GroceriesPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.stories.tsx
new file mode 100644
index 0000000000..d9631ae657
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import HomePage from './HomePage'
+
+const meta: Meta = {
+ component: HomePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.test.tsx
new file mode 100644
index 0000000000..a09c9218fb
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import HomePage from './HomePage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('HomePage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..d86fe65702
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/HomePage/HomePage.tsx
@@ -0,0 +1,7 @@
+import BlogPostsCell from 'src/components/BlogPostsCell'
+
+const HomePage = () => {
+ return
+}
+
+export default HomePage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/LoginPage/LoginPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000000..fced4f6181
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/LoginPage/LoginPage.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useRef } from 'react'
+
+import {
+ Form,
+ Label,
+ TextField,
+ PasswordField,
+ Submit,
+ FieldError,
+} from '@cedarjs/forms'
+import { Link, navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const LoginPage = () => {
+ const { isAuthenticated, logIn } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await logIn({
+ username: data.username,
+ password: data.password,
+ })
+
+ if (response.message) {
+ toast(response.message)
+ } else if (response.error) {
+ toast.error(response.error)
+ } else {
+ toast.success('Welcome back!')
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Don't have an account? {' '}
+
+ Sign up!
+
+
+
+
+ >
+ )
+}
+
+export default LoginPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..92ef916989
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,44 @@
+export default () => (
+
+
+
+
+ 404 Page Not Found
+
+
+
+)
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
new file mode 100644
index 0000000000..f3f8c7bfc8
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
@@ -0,0 +1,11 @@
+import EditPostCell from 'src/components/Post/EditPostCell'
+
+type PostPageProps = {
+ id: number
+}
+
+const EditPostPage = ({ id }: PostPageProps) => {
+ return
+}
+
+export default EditPostPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
new file mode 100644
index 0000000000..0b3c453cc3
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
@@ -0,0 +1,7 @@
+import NewPost from 'src/components/Post/NewPost'
+
+const NewPostPage = () => {
+ return
+}
+
+export default NewPostPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostPage/PostPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostPage/PostPage.tsx
new file mode 100644
index 0000000000..ca4048740a
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostPage/PostPage.tsx
@@ -0,0 +1,11 @@
+import PostCell from 'src/components/Post/PostCell'
+
+type PostPageProps = {
+ id: number
+}
+
+const PostPage = ({ id }: PostPageProps) => {
+ return
+}
+
+export default PostPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
new file mode 100644
index 0000000000..f5b3668d40
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
@@ -0,0 +1,7 @@
+import PostsCell from 'src/components/Post/PostsCell'
+
+const PostsPage = () => {
+ return
+}
+
+export default PostsPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
new file mode 100644
index 0000000000..ebc171846e
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ProfilePage from './ProfilePage'
+
+const meta: Meta = {
+ component: ProfilePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
new file mode 100644
index 0000000000..027edfd387
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
@@ -0,0 +1,21 @@
+import { render, waitFor, screen } from '@cedarjs/testing/web'
+
+import ProfilePage from './ProfilePage'
+
+describe('ProfilePage', () => {
+ it('renders successfully', async () => {
+ mockCurrentUser({
+ email: 'danny@bazinga.com',
+ id: 84849020,
+ roles: 'BAZINGA',
+ })
+
+ await waitFor(async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ expect(await screen.findByText('danny@bazinga.com')).toBeInTheDocument()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
new file mode 100644
index 0000000000..0209c00efe
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
@@ -0,0 +1,55 @@
+import { Metadata } from '@cedarjs/web'
+
+import { useAuth } from 'src/auth'
+// import { Link, routes } from '@cedarjs/router'
+
+const ProfilePage = () => {
+ const { currentUser, isAuthenticated, hasRole, loading } = useAuth()
+
+ if (loading) {
+ return Loading...
+ }
+
+ return (
+ <>
+
+
+ Profile
+
+
+
+
+ Key
+ Value
+
+
+
+
+ ID
+ {currentUser.id}
+
+
+ ROLES
+ {currentUser.roles}
+
+
+ EMAIL
+ {currentUser.email}
+
+
+
+ isAuthenticated
+ {JSON.stringify(isAuthenticated)}
+
+
+
+ Is Admin
+ {JSON.stringify(hasRole('ADMIN'))}
+
+
+
+ >
+ )
+}
+
+export default ProfilePage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
new file mode 100644
index 0000000000..fd45ae810f
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
@@ -0,0 +1,115 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { Form, Label, PasswordField, Submit, FieldError } from '@cedarjs/forms'
+import { navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
+ const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
+ useAuth()
+ const [enabled, setEnabled] = useState(true)
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ useEffect(() => {
+ const validateToken = async () => {
+ const response = await validateResetToken(resetToken)
+ if (response.error) {
+ setEnabled(false)
+ toast.error(response.error)
+ } else {
+ setEnabled(true)
+ }
+ }
+ validateToken()
+ }, [resetToken, validateResetToken])
+
+ const passwordRef = useRef(null)
+ useEffect(() => {
+ passwordRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await resetPassword({
+ resetToken,
+ password: data.password,
+ })
+
+ if (response.error) {
+ toast.error(response.error)
+ } else {
+ toast.success('Password changed!')
+ await reauthenticate()
+ navigate(routes.login())
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default ResetPasswordPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/SignupPage/SignupPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/SignupPage/SignupPage.tsx
new file mode 100644
index 0000000000..8d4998b7e4
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/SignupPage/SignupPage.tsx
@@ -0,0 +1,147 @@
+import { useEffect, useRef } from 'react'
+
+import {
+ Form,
+ Label,
+ TextField,
+ PasswordField,
+ FieldError,
+ Submit,
+} from '@cedarjs/forms'
+import { Link, navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const SignupPage = () => {
+ const { isAuthenticated, signUp } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ // focus on username box on page load
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await signUp({
+ username: data.username,
+ password: data.password,
+ 'full-name': data['full-name'],
+ })
+
+ if (response.message) {
+ toast(response.message)
+ } else if (response.error) {
+ toast.error(response.error)
+ } else {
+ // user is signed in automatically
+ toast.success('Welcome!')
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Already have an account? {' '}
+
+ Log in!
+
+
+
+
+ >
+ )
+}
+
+export default SignupPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
new file mode 100644
index 0000000000..88a6dd0b61
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
@@ -0,0 +1,3 @@
+export async function routeParameters() {
+ return [{ id: 2 }]
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
new file mode 100644
index 0000000000..9b15c73474
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import WaterfallPage from './WaterfallPage'
+
+const meta: Meta = {
+ component: WaterfallPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
new file mode 100644
index 0000000000..211c1a684a
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import WaterfallPage from './WaterfallPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('WaterfallPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
new file mode 100644
index 0000000000..6c4f24a14c
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
@@ -0,0 +1,11 @@
+import WaterfallBlogPostCell from 'src/components/WaterfallBlogPostCell'
+
+type WaterfallPageProps = {
+ id: number
+}
+
+const WaterfallPage = ({ id }: WaterfallPageProps) => (
+
+)
+
+export default WaterfallPage
diff --git a/__fixtures__/esm-fragment-test-project/web/src/scaffold.css b/__fixtures__/esm-fragment-test-project/web/src/scaffold.css
new file mode 100644
index 0000000000..ffa9142b71
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/src/scaffold.css
@@ -0,0 +1,243 @@
+.rw-scaffold {
+ @apply bg-white text-gray-600;
+}
+.rw-scaffold h1,
+.rw-scaffold h2 {
+ @apply m-0;
+}
+.rw-scaffold a {
+ @apply bg-transparent;
+}
+.rw-scaffold ul {
+ @apply m-0 p-0;
+}
+.rw-scaffold input:-ms-input-placeholder {
+ @apply text-gray-500;
+}
+.rw-scaffold input::-ms-input-placeholder {
+ @apply text-gray-500;
+}
+.rw-scaffold input::placeholder {
+ @apply text-gray-500;
+}
+.rw-header {
+ @apply flex justify-between px-8 py-4;
+}
+.rw-main {
+ @apply mx-4 pb-4;
+}
+.rw-segment {
+ @apply w-full overflow-hidden rounded-lg border border-gray-200;
+ scrollbar-color: theme('colors.zinc.400') transparent;
+}
+.rw-segment::-webkit-scrollbar {
+ height: initial;
+}
+.rw-segment::-webkit-scrollbar-track {
+ @apply rounded-b-[10px] rounded-t-none border-0 border-t border-solid border-gray-200 bg-transparent p-[2px];
+}
+.rw-segment::-webkit-scrollbar-thumb {
+ @apply rounded-full border-[3px] border-solid border-transparent bg-zinc-400 bg-clip-content;
+}
+.rw-segment-header {
+ @apply bg-gray-200 px-4 py-3 text-gray-700;
+}
+.rw-segment-main {
+ @apply bg-gray-100 p-4;
+}
+.rw-link {
+ @apply text-blue-400 underline;
+}
+.rw-link:hover {
+ @apply text-blue-500;
+}
+.rw-forgot-link {
+ @apply mt-1 text-right text-xs text-gray-400 underline;
+}
+.rw-forgot-link:hover {
+ @apply text-blue-500;
+}
+.rw-heading {
+ @apply font-semibold;
+}
+.rw-heading.rw-heading-primary {
+ @apply text-xl;
+}
+.rw-heading.rw-heading-secondary {
+ @apply text-sm;
+}
+.rw-heading .rw-link {
+ @apply text-gray-600 no-underline;
+}
+.rw-heading .rw-link:hover {
+ @apply text-gray-900 underline;
+}
+.rw-cell-error {
+ @apply text-sm font-semibold;
+}
+.rw-form-wrapper {
+ @apply -mt-4 text-sm;
+}
+.rw-cell-error,
+.rw-form-error-wrapper {
+ @apply my-4 rounded border border-red-100 bg-red-50 p-4 text-red-600;
+}
+.rw-form-error-title {
+ @apply m-0 font-semibold;
+}
+.rw-form-error-list {
+ @apply mt-2 list-inside list-disc;
+}
+.rw-button {
+ @apply flex cursor-pointer justify-center rounded border-0 bg-gray-200 px-4 py-1 text-xs font-semibold uppercase leading-loose tracking-wide text-gray-500 no-underline transition duration-100;
+}
+.rw-button:hover {
+ @apply bg-gray-500 text-white;
+}
+.rw-button.rw-button-small {
+ @apply rounded-sm px-2 py-1 text-xs;
+}
+.rw-button.rw-button-green {
+ @apply bg-green-500 text-white;
+}
+.rw-button.rw-button-green:hover {
+ @apply bg-green-700;
+}
+.rw-button.rw-button-blue {
+ @apply bg-blue-500 text-white;
+}
+.rw-button.rw-button-blue:hover {
+ @apply bg-blue-700;
+}
+.rw-button.rw-button-red {
+ @apply bg-red-500 text-white;
+}
+.rw-button.rw-button-red:hover {
+ @apply bg-red-700 text-white;
+}
+.rw-button-icon {
+ @apply mr-1 text-xl leading-5;
+}
+.rw-button-group {
+ @apply mx-2 my-3 flex justify-center;
+}
+.rw-button-group .rw-button {
+ @apply mx-1;
+}
+.rw-form-wrapper .rw-button-group {
+ @apply mt-8;
+}
+.rw-label {
+ @apply mt-6 block text-left font-semibold text-gray-600;
+}
+.rw-label.rw-label-error {
+ @apply text-red-600;
+}
+.rw-input {
+ @apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none;
+}
+.rw-check-radio-items {
+ @apply flex justify-items-center;
+}
+.rw-check-radio-item-none {
+ @apply text-gray-600;
+}
+.rw-input[type='checkbox'],
+.rw-input[type='radio'] {
+ @apply ml-0 mr-1 mt-1 inline w-4;
+}
+.rw-input:focus {
+ @apply border-gray-400;
+}
+.rw-input-error {
+ @apply border-red-600 text-red-600;
+}
+.rw-input-error:focus {
+ @apply border-red-600 outline-none;
+ box-shadow: 0 0 5px #c53030;
+}
+.rw-field-error {
+ @apply mt-1 block text-xs font-semibold uppercase text-red-600;
+}
+.rw-table-wrapper-responsive {
+ @apply overflow-x-auto;
+}
+.rw-table-wrapper-responsive .rw-table {
+ min-width: 48rem;
+}
+.rw-table {
+ @apply w-full text-sm;
+}
+.rw-table th,
+.rw-table td {
+ @apply p-3;
+}
+.rw-table td {
+ @apply bg-white text-gray-900;
+}
+.rw-table tr:nth-child(odd) td,
+.rw-table tr:nth-child(odd) th {
+ @apply bg-gray-50;
+}
+.rw-table thead tr {
+ @apply bg-gray-200 text-gray-600;
+}
+.rw-table th {
+ @apply text-left font-semibold;
+}
+.rw-table thead th {
+ @apply text-left;
+}
+.rw-table tbody th {
+ @apply text-right;
+}
+@media (min-width: 768px) {
+ .rw-table tbody th {
+ @apply w-1/5;
+ }
+}
+.rw-table tbody tr {
+ @apply border-t border-gray-200;
+}
+.rw-table input {
+ @apply ml-0;
+}
+.rw-table-actions {
+ @apply flex h-4 items-center justify-end pr-1;
+}
+.rw-table-actions .rw-button {
+ @apply bg-transparent;
+}
+.rw-table-actions .rw-button:hover {
+ @apply bg-gray-500 text-white;
+}
+.rw-table-actions .rw-button-blue {
+ @apply text-blue-500;
+}
+.rw-table-actions .rw-button-blue:hover {
+ @apply bg-blue-500 text-white;
+}
+.rw-table-actions .rw-button-red {
+ @apply text-red-600;
+}
+.rw-table-actions .rw-button-red:hover {
+ @apply bg-red-600 text-white;
+}
+.rw-text-center {
+ @apply text-center;
+}
+.rw-login-container {
+ @apply mx-auto my-16 flex w-96 flex-wrap items-center justify-center;
+}
+.rw-login-container .rw-form-wrapper {
+ @apply w-full text-center;
+}
+.rw-login-link {
+ @apply mt-4 w-full text-center text-sm text-gray-600;
+}
+.rw-webauthn-wrapper {
+ @apply mx-4 mt-6 leading-6;
+}
+.rw-webauthn-wrapper h2 {
+ @apply mb-4 text-xl font-bold;
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/tsconfig.json b/__fixtures__/esm-fragment-test-project/web/tsconfig.json
new file mode 100644
index 0000000000..edfcaff396
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/web/src",
+ "../api/src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/web/src/*",
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$api/*": ["../api/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/web"]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types",
+ "../node_modules/@testing-library"
+ ],
+ "types": ["jest", "jest-dom"],
+ "jsx": "preserve"
+ },
+ "include": [
+ "src",
+ "config",
+ ".storybook/**/*",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types",
+ "./types"
+ ]
+}
diff --git a/__fixtures__/esm-fragment-test-project/web/vite.config.ts b/__fixtures__/esm-fragment-test-project/web/vite.config.ts
new file mode 100644
index 0000000000..8db6f5be72
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/vite.config.ts
@@ -0,0 +1,21 @@
+///
+
+import dns from 'node:dns'
+
+import { defineConfig } from 'vite'
+
+import { cedar } from '@cedarjs/vite'
+
+// So that Vite will load on localhost instead of `127.0.0.1`.
+// See: https://vitejs.dev/config/server-options.html#server-host.
+dns.setDefaultResultOrder('verbatim')
+
+export default defineConfig(({ mode }) => ({
+ plugins: [cedar({ mode })],
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./vitest.setup.ts'],
+ // Enables global test APIs like describe, it, expect
+ globals: true,
+ },
+}))
diff --git a/__fixtures__/esm-fragment-test-project/web/vitest.setup.ts b/__fixtures__/esm-fragment-test-project/web/vitest.setup.ts
new file mode 100644
index 0000000000..3c2a688f47
--- /dev/null
+++ b/__fixtures__/esm-fragment-test-project/web/vitest.setup.ts
@@ -0,0 +1,12 @@
+import '@testing-library/jest-dom/vitest'
+
+import { cleanup } from '@testing-library/react'
+import { afterEach } from 'vitest'
+
+afterEach(() => {
+ // If vitest globals are enabled testing-library will clean up after each
+ // test automatically, but we don't enable globals, so we have to manually
+ // clean up here
+ // https://testing-library.com/docs/react-testing-library/api/#cleanup
+ cleanup()
+})
diff --git a/__fixtures__/esm-test-project/.editorconfig b/__fixtures__/esm-test-project/.editorconfig
new file mode 100644
index 0000000000..ae10a5cce3
--- /dev/null
+++ b/__fixtures__/esm-test-project/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/__fixtures__/esm-test-project/.env.defaults b/__fixtures__/esm-test-project/.env.defaults
new file mode 100644
index 0000000000..fb88fb33b3
--- /dev/null
+++ b/__fixtures__/esm-test-project/.env.defaults
@@ -0,0 +1,19 @@
+# These environment variables will be used by default if you do not create any
+# yourself in .env. This file should be safe to check into your version control
+# system. Any custom values should go in .env and .env should *not* be checked
+# into version control.
+
+# schema.prisma defaults
+DATABASE_URL=file:./dev.db
+
+# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
+# TEST_DATABASE_URL=file:./.redwood/test.db
+
+# disables Prisma CLI update notifier
+PRISMA_HIDE_UPDATE_MESSAGE=true
+
+# Option to override the current environment's default api-side log level
+# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise.
+# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
+# Ordered by how verbose they are: trace | debug | info | warn | error | silent
+# LOG_LEVEL=debug
diff --git a/__fixtures__/esm-test-project/.env.example b/__fixtures__/esm-test-project/.env.example
new file mode 100644
index 0000000000..2a2de6c026
--- /dev/null
+++ b/__fixtures__/esm-test-project/.env.example
@@ -0,0 +1,4 @@
+# DATABASE_URL=file:./dev.db
+# TEST_DATABASE_URL=file:./.redwood/test.db
+# PRISMA_HIDE_UPDATE_MESSAGE=true
+# LOG_LEVEL=trace
diff --git a/__fixtures__/esm-test-project/.gitignore b/__fixtures__/esm-test-project/.gitignore
new file mode 100644
index 0000000000..31d9637ede
--- /dev/null
+++ b/__fixtures__/esm-test-project/.gitignore
@@ -0,0 +1,24 @@
+.idea
+.DS_Store
+.env*
+!.env.example
+!.env.defaults
+.netlify
+.redwood/*
+!.redwood/README.md
+dev.db*
+dist
+dist-babel
+node_modules
+yarn-error.log
+web/public/mockServiceWorker.js
+web/types/graphql.d.ts
+api/types/graphql.d.ts
+api/src/lib/generateGraphiQLHeader.*
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/__fixtures__/esm-test-project/.redwood/README.md b/__fixtures__/esm-test-project/.redwood/README.md
new file mode 100644
index 0000000000..8a1bf5738b
--- /dev/null
+++ b/__fixtures__/esm-test-project/.redwood/README.md
@@ -0,0 +1,44 @@
+# .redwood
+
+## What is this directory?
+
+Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project.
+
+## Do I need to do anything with this directory?
+
+No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood.
+
+You don't need to commit any other contents of this directory to your version control system. It's ignored by default.
+
+## What's in this directory?
+
+### Files
+
+| Name | Description |
+| :---------------- | :----------------------------------------------------------------------------------------------------------------- |
+| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. |
+| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. |
+| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. |
+| test.db | The sqlite database used when running tests. |
+
+### Directories
+
+| Name | Description |
+| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
+| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. |
+| logs | Stores log files for background tasks such as update checking. |
+| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. |
+| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. |
+| types | Stores the results of type generation. |
+| updateCheck | Stores a file which contains the results of checking for Redwood updates. |
+| studio | Used to store data for `rw studio` |
+
+We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list.
+
+### Telemetry
+
+RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com).
+
+### Have any questions?
+
+Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions.
diff --git a/__fixtures__/esm-test-project/.vscode/extensions.json b/__fixtures__/esm-test-project/.vscode/extensions.json
new file mode 100644
index 0000000000..6e458a9231
--- /dev/null
+++ b/__fixtures__/esm-test-project/.vscode/extensions.json
@@ -0,0 +1,16 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ofhumanbondage.react-proptypes-intellisense",
+ "mgmcdermott.vscode-language-babel",
+ "wix.vscode-import-cost",
+ "pflannery.vscode-versionlens",
+ "editorconfig.editorconfig",
+ "prisma.prisma",
+ "graphql.vscode-graphql",
+ "csstools.postcss",
+ "bradlc.vscode-tailwindcss"
+ ],
+ "unwantedRecommendations": []
+}
\ No newline at end of file
diff --git a/__fixtures__/esm-test-project/.vscode/launch.json b/__fixtures__/esm-test-project/.vscode/launch.json
new file mode 100644
index 0000000000..37257c5c15
--- /dev/null
+++ b/__fixtures__/esm-test-project/.vscode/launch.json
@@ -0,0 +1,56 @@
+{
+ "version": "0.3.0",
+ "configurations": [
+ {
+ "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening
+ "name": "Run Dev Server",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "name": "Attach API debugger",
+ "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration
+ "request": "attach",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node",
+ "localRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "remoteRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "sourceMaps": true,
+ "restart": true,
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "name": "Launch Web debugger",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:8910",
+ "webRoot": "${workspaceRoot}/web/src",
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "command": "yarn redwood test api",
+ "name": "Test api",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "command": "yarn redwood test web",
+ "name": "Test web",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ ],
+ "compounds": [
+ {
+ "name": "Start Debug",
+ "configurations": [
+ "Run Dev Server",
+ "Attach API debugger",
+ "Launch Web debugger"
+ ],
+ "stopAll": true
+ }
+ ]
+}
diff --git a/__fixtures__/esm-test-project/.vscode/settings.json b/__fixtures__/esm-test-project/.vscode/settings.json
new file mode 100644
index 0000000000..1d3afa8ae3
--- /dev/null
+++ b/__fixtures__/esm-test-project/.vscode/settings.json
@@ -0,0 +1,17 @@
+{
+ "editor.tabSize": 2,
+ "files.trimTrailingWhitespace": true,
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ },
+ "[prisma]": {
+ "editor.formatOnSave": true
+ },
+ "tailwindCSS.classAttributes": [
+ "class",
+ "className",
+ "activeClassName",
+ "errorClassName"
+ ]
+}
diff --git a/__fixtures__/esm-test-project/.vscode/tasks.json b/__fixtures__/esm-test-project/.vscode/tasks.json
new file mode 100644
index 0000000000..549249ec63
--- /dev/null
+++ b/__fixtures__/esm-test-project/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "WaitForDevServer",
+ "group": "none",
+ "type": "shell",
+ "command": "bash",
+ "args": [
+ "-c",
+ "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;"
+ ],
+ "windows": {
+ "command": "powershell",
+ "args": [
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };"
+ ]
+ },
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "panel": "shared",
+ "close": true
+ }
+ },
+ ]
+}
diff --git a/__fixtures__/esm-test-project/.yarnrc.yml b/__fixtures__/esm-test-project/.yarnrc.yml
new file mode 100644
index 0000000000..e8c5d50aa7
--- /dev/null
+++ b/__fixtures__/esm-test-project/.yarnrc.yml
@@ -0,0 +1,15 @@
+# Yarn's manifest file. You can configure yarn here.
+# See https://yarnpkg.com/configuration/yarnrc.
+
+# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option.
+compressionLevel: 0
+
+enableGlobalCache: true
+
+# Lets yarn use hardlinks inside `node_modules` to dedupe packages.
+# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store.
+nmMode: hardlinks-local
+
+# How to install Node packages.
+# Heads up: right now, Redwood expects this to be `node-modules`.
+nodeLinker: node-modules
diff --git a/__fixtures__/esm-test-project/README.md b/__fixtures__/esm-test-project/README.md
new file mode 100644
index 0000000000..4487e7f63d
--- /dev/null
+++ b/__fixtures__/esm-test-project/README.md
@@ -0,0 +1,17 @@
+# README
+
+Welcome to your new [CedarJS](https://cedarjs.com) project!
+
+Start by installing dependencies:
+
+```
+yarn install
+```
+
+Then start the development server:
+
+```
+yarn redwood dev
+```
+
+Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources.
diff --git a/__fixtures__/esm-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql b/__fixtures__/esm-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
new file mode 100644
index 0000000000..9dd73df9b6
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql
@@ -0,0 +1,34 @@
+-- CreateTable
+CREATE TABLE "UserExample" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "name" TEXT
+);
+
+-- CreateTable
+CREATE TABLE "Post" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "title" TEXT NOT NULL,
+ "body" TEXT NOT NULL,
+ "authorId" INTEGER NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "hashedPassword" TEXT NOT NULL,
+ "fullName" TEXT NOT NULL,
+ "salt" TEXT NOT NULL,
+ "resetToken" TEXT,
+ "resetTokenExpiresAt" DATETIME,
+ "roles" TEXT
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/__fixtures__/esm-test-project/api/db/migrations/20220102120000_create_contact/migration.sql b/__fixtures__/esm-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
new file mode 100644
index 0000000000..8d7bd91beb
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/db/migrations/20220102120000_create_contact/migration.sql
@@ -0,0 +1,8 @@
+-- CreateTable
+CREATE TABLE "Contact" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "name" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "message" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/__fixtures__/esm-test-project/api/db/migrations/migration_lock.toml b/__fixtures__/esm-test-project/api/db/migrations/migration_lock.toml
new file mode 100644
index 0000000000..e5e5c4705a
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/db/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite"
\ No newline at end of file
diff --git a/__fixtures__/esm-test-project/api/db/schema.prisma b/__fixtures__/esm-test-project/api/db/schema.prisma
new file mode 100644
index 0000000000..c0ebb5b0e5
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/db/schema.prisma
@@ -0,0 +1,53 @@
+// Don't forget to tell Prisma about your edits to this file using
+// `yarn rw prisma migrate dev` or `yarn rw prisma db push`.
+// `migrate` is like committing while `push` is for prototyping.
+// Read more about both here:
+// https://www.prisma.io/docs/orm/prisma-migrate
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+// Define your own datamodels here and run `yarn redwood prisma migrate dev`
+// to create migrations for them and apply to your dev DB.
+// TODO: Please remove the following example:
+model UserExample {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ title String
+ body String
+ authorId Int
+ author User @relation(fields: [authorId], references: [id])
+ createdAt DateTime @default(now())
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ hashedPassword String
+ fullName String
+ salt String
+ resetToken String?
+ resetTokenExpiresAt DateTime?
+ roles String?
+ posts Post[]
+}
+
+model Contact {
+ id Int @id @default(autoincrement())
+ name String
+ email String
+ message String
+ createdAt DateTime @default(now())
+}
diff --git a/__fixtures__/esm-test-project/api/package.json b/__fixtures__/esm-test-project/api/package.json
new file mode 100644
index 0000000000..f3b9e4e1cb
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "api",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@cedarjs/api": "0.0.5",
+ "@cedarjs/auth-dbauth-api": "0.0.5",
+ "@cedarjs/graphql-server": "0.0.5"
+ }
+}
diff --git a/__fixtures__/esm-test-project/api/src/__tests__/1-db-import.test.ts b/__fixtures__/esm-test-project/api/src/__tests__/1-db-import.test.ts
new file mode 100644
index 0000000000..2c685fd85d
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/__tests__/1-db-import.test.ts
@@ -0,0 +1,5 @@
+test('Cedar is correctly tracking db imports 1', () => {
+ // It'll currently be `undefined` here, but all that's important is that it's
+ // falsy
+ expect(globalThis.__cedarjs_db_imported__).toBeFalsy()
+})
diff --git a/__fixtures__/esm-test-project/api/src/__tests__/2-db-import.test.ts b/__fixtures__/esm-test-project/api/src/__tests__/2-db-import.test.ts
new file mode 100644
index 0000000000..1c10dbd22f
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/__tests__/2-db-import.test.ts
@@ -0,0 +1,10 @@
+import { posts } from 'src/services/posts/posts.js'
+
+test('Cedar is correctly tracking db imports 2', async () => {
+ const allPosts = await posts()
+ expect(Array.isArray(allPosts)).toEqual(true)
+
+ // Because we're importing the posts service, which uses the database, Cedar
+ // should track that db import
+ expect(globalThis.__cedarjs_db_imported__).toBeTruthy()
+})
diff --git a/__fixtures__/esm-test-project/api/src/__tests__/3-db-import.test.ts b/__fixtures__/esm-test-project/api/src/__tests__/3-db-import.test.ts
new file mode 100644
index 0000000000..fb4be804fd
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/__tests__/3-db-import.test.ts
@@ -0,0 +1,5 @@
+test('Cedar is correctly tracking db imports 1', () => {
+ // The previous test (2-db-import.test.ts) imported the database but this one
+ // doesn't, so __cedarjs_db_imported__ should be falsy again
+ expect(globalThis.__cedarjs_db_imported__).toBeFalsy()
+})
diff --git a/__fixtures__/esm-test-project/api/src/__tests__/context.test.ts b/__fixtures__/esm-test-project/api/src/__tests__/context.test.ts
new file mode 100644
index 0000000000..972c4756e8
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/__tests__/context.test.ts
@@ -0,0 +1,14 @@
+test('Set a mock user on the context', async () => {
+ const user = {
+ id: 0o7,
+ name: 'Bond, James Bond',
+ email: 'totallyNotASpy@example.com',
+ roles: 'secret_agent',
+ }
+ mockCurrentUser(user)
+ expect(context.currentUser).toStrictEqual(user)
+})
+
+test('Context is isolated between tests', () => {
+ expect(context).toStrictEqual({})
+})
diff --git a/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.test.ts b/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.test.ts
new file mode 100644
index 0000000000..83b683768b
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.test.ts
@@ -0,0 +1,20 @@
+import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
+
+import requireAuth from './requireAuth.js'
+
+describe('requireAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(requireAuth.schema).toBeTruthy()
+ expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
+ })
+
+ it('requireAuth has stub implementation. Should not throw when current user', () => {
+ // If you want to set values in context, pass it through e.g.
+ // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
+ const mockExecution = mockRedwoodDirective(requireAuth, {
+ context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } },
+ })
+
+ expect(mockExecution).not.toThrowError()
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.ts b/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.ts
new file mode 100644
index 0000000000..3dadf21e68
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/directives/requireAuth/requireAuth.ts
@@ -0,0 +1,25 @@
+import { gql } from 'graphql-tag'
+
+import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+import { requireAuth as applicationRequireAuth } from 'src/lib/auth.js'
+
+export const schema = gql`
+ """
+ Use to check whether or not a user is authenticated and is associated
+ with an optional set of roles.
+ """
+ directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+`
+
+type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
+
+const validate: RequireAuthValidate = ({ directiveArgs }) => {
+ const { roles } = directiveArgs
+ applicationRequireAuth({ roles })
+}
+
+const requireAuth = createValidatorDirective(schema, validate)
+
+export default requireAuth
diff --git a/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.test.ts b/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.test.ts
new file mode 100644
index 0000000000..68c006bdae
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.test.ts
@@ -0,0 +1,10 @@
+import { getDirectiveName } from '@cedarjs/testing/api'
+
+import skipAuth from './skipAuth.js'
+
+describe('skipAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(skipAuth.schema).toBeTruthy()
+ expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.ts b/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.ts
new file mode 100644
index 0000000000..fdea5cf17b
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/directives/skipAuth/skipAuth.ts
@@ -0,0 +1,16 @@
+import { gql } from 'graphql-tag'
+
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+export const schema = gql`
+ """
+ Use to skip authentication checks and allow public access.
+ """
+ directive @skipAuth on FIELD_DEFINITION
+`
+
+const skipAuth = createValidatorDirective(schema, () => {
+ return
+})
+
+export default skipAuth
diff --git a/__fixtures__/esm-test-project/api/src/functions/auth.ts b/__fixtures__/esm-test-project/api/src/functions/auth.ts
new file mode 100644
index 0000000000..b2192a0dcf
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/functions/auth.ts
@@ -0,0 +1,202 @@
+import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
+
+import { DbAuthHandler } from '@cedarjs/auth-dbauth-api'
+import type { DbAuthHandlerOptions, UserType } from '@cedarjs/auth-dbauth-api'
+
+import { cookieName } from 'src/lib/auth.js'
+import { db } from 'src/lib/db.js'
+
+export const handler = async (
+ event: APIGatewayProxyEvent,
+ context: Context
+) => {
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
+ // handler() is invoked after verifying that a user was found with the given
+ // username. This is where you can send the user an email with a link to
+ // reset their password. With the default dbAuth routes and field names, the
+ // URL to reset the password will be:
+ //
+ // https://example.com/reset-password?resetToken=${user.resetToken}
+ //
+ // Whatever is returned from this function will be returned from
+ // the `forgotPassword()` function that is destructured from `useAuth()`.
+ // You could use this return value to, for example, show the email
+ // address in a toast message so the user will know it worked and where
+ // to look for the email.
+ //
+ // Note that this return value is sent to the client in *plain text*
+ // so don't include anything you wouldn't want prying eyes to see. The
+ // `user` here has been sanitized to only include the fields listed in
+ // `allowedUserFields` so it should be safe to return as-is.
+ handler: (user, _resetToken) => {
+ // TODO: Send user an email/message with a link to reset their password,
+ // including the `resetToken`. The URL should look something like:
+ // `http://localhost:8910/reset-password?resetToken=${resetToken}`
+
+ return user
+ },
+
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
+ expires: 60 * 60 * 24,
+
+ errors: {
+ // for security reasons you may want to be vague here rather than expose
+ // the fact that the email address wasn't found (prevents fishing for
+ // valid email addresses)
+ usernameNotFound: 'Username not found',
+ // if the user somehow gets around client validation
+ usernameRequired: 'Username is required',
+ },
+ }
+
+ const loginOptions: DbAuthHandlerOptions['login'] = {
+ // handler() is called after finding the user that matches the
+ // username/password provided at login, but before actually considering them
+ // logged in. The `user` argument will be the user in the database that
+ // matched the username/password.
+ //
+ // If you want to allow this user to log in simply return the user.
+ //
+ // If you want to prevent someone logging in for another reason (maybe they
+ // didn't validate their email yet), throw an error and it will be returned
+ // by the `logIn()` function from `useAuth()` in the form of:
+ // `{ message: 'Error message' }`
+ handler: (user) => {
+ return user
+ },
+
+ errors: {
+ usernameOrPasswordMissing: 'Both username and password are required',
+ usernameNotFound: 'Username ${username} not found',
+ // For security reasons you may want to make this the same as the
+ // usernameNotFound error so that a malicious user can't use the error
+ // to narrow down if it's the username or password that's incorrect
+ incorrectPassword: 'Incorrect password for ${username}',
+ },
+
+ // How long a user will remain logged in, in seconds
+ expires: 60 * 60 * 24 * 365 * 10,
+ }
+
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
+ // handler() is invoked after the password has been successfully updated in
+ // the database. Returning anything truthy will automatically log the user
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
+ // user to the login page.
+ handler: (_user) => {
+ return true
+ },
+
+ // If `false` then the new password MUST be different from the current one
+ allowReusedPassword: true,
+
+ errors: {
+ // the resetToken is valid, but expired
+ resetTokenExpired: 'resetToken is expired',
+ // no user was found with the given resetToken
+ resetTokenInvalid: 'resetToken is invalid',
+ // the resetToken was not present in the URL
+ resetTokenRequired: 'resetToken is required',
+ // new password is the same as the old password (apparently they did not forget it)
+ reusedPassword: 'Must choose a new password',
+ },
+ }
+
+ interface UserAttributes {
+ 'full-name': string
+ }
+
+ const signupOptions: DbAuthHandlerOptions<
+ UserType,
+ UserAttributes
+ >['signup'] = {
+ // Whatever you want to happen to your data on new user signup. Redwood will
+ // check for duplicate usernames before calling this handler. At a minimum
+ // you need to save the `username`, `hashedPassword` and `salt` to your
+ // user table. `userAttributes` contains any additional object members that
+ // were included in the object given to the `signUp()` function you got
+ // from `useAuth()`.
+ //
+ // If you want the user to be immediately logged in, return the user that
+ // was created.
+ //
+ // If this handler throws an error, it will be returned by the `signUp()`
+ // function in the form of: `{ error: 'Error message' }`.
+ //
+ // If this returns anything else, it will be returned by the
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
+ handler: ({ username, hashedPassword, salt, userAttributes }) => {
+ return db.user.create({
+ data: {
+ email: username,
+ hashedPassword: hashedPassword,
+ salt: salt,
+ fullName: userAttributes['full-name'],
+ },
+ })
+ },
+
+ // Include any format checks for password here. Return `true` if the
+ // password is valid, otherwise throw a `PasswordValidationError`.
+ // Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
+ passwordValidation: (_password) => {
+ return true
+ },
+
+ errors: {
+ // `field` will be either "username" or "password"
+ fieldMissing: '${field} is required',
+ usernameTaken: 'Username `${username}` already in use',
+ },
+ }
+
+ const authHandler = new DbAuthHandler(event, context, {
+ // Provide prisma db client
+ db: db,
+
+ // The name of the property you'd call on `db` to access your user table.
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
+ authModelAccessor: 'user',
+
+ // A map of what dbAuth calls a field to what your database calls it.
+ // `id` is whatever column you use to uniquely identify a user (probably
+ // something like `id` or `userId` or even `email`)
+ authFields: {
+ id: 'id',
+ username: 'email',
+ hashedPassword: 'hashedPassword',
+ salt: 'salt',
+ resetToken: 'resetToken',
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
+ },
+
+ // A list of fields on your user object that are safe to return to the
+ // client when invoking a handler that returns a user (like forgotPassword
+ // and signup). This list should be as small as possible to be sure not to
+ // leak any sensitive information to the client.
+ allowedUserFields: ['id', 'email'],
+
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
+ cookie: {
+ attributes: {
+ HttpOnly: true,
+ Path: '/',
+ SameSite: 'Lax',
+ Secure: process.env.NODE_ENV !== 'development',
+
+ // If you need to allow other domains (besides the api side) access to
+ // the dbAuth session cookie:
+ // Domain: 'example.com',
+ },
+ name: cookieName,
+ },
+
+ forgotPassword: forgotPasswordOptions,
+ login: loginOptions,
+ resetPassword: resetPasswordOptions,
+ signup: signupOptions,
+ })
+
+ return await authHandler.invoke()
+}
diff --git a/__fixtures__/esm-test-project/api/src/functions/graphql.ts b/__fixtures__/esm-test-project/api/src/functions/graphql.ts
new file mode 100644
index 0000000000..4445ef3976
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/functions/graphql.ts
@@ -0,0 +1,25 @@
+import { createAuthDecoder } from '@cedarjs/auth-dbauth-api'
+import { createGraphQLHandler } from '@cedarjs/graphql-server'
+
+import directives from 'src/directives/**/*.{js,ts}'
+import sdls from 'src/graphql/**/*.sdl.{js,ts}'
+import services from 'src/services/**/*.{js,ts}'
+
+import { cookieName, getCurrentUser } from 'src/lib/auth.js'
+import { db } from 'src/lib/db.js'
+import { logger } from 'src/lib/logger.js'
+
+const authDecoder = createAuthDecoder(cookieName)
+
+export const handler = createGraphQLHandler({
+ authDecoder,
+ getCurrentUser,
+ loggerConfig: { logger, options: {} },
+ directives,
+ sdls,
+ services,
+ onException: () => {
+ // Disconnect from your database with an unhandled exception.
+ db.$disconnect()
+ },
+})
diff --git a/__fixtures__/esm-test-project/api/src/graphql/.keep b/__fixtures__/esm-test-project/api/src/graphql/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-test-project/api/src/graphql/contacts.sdl.ts b/__fixtures__/esm-test-project/api/src/graphql/contacts.sdl.ts
new file mode 100644
index 0000000000..7dec262a57
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/graphql/contacts.sdl.ts
@@ -0,0 +1,32 @@
+export const schema = gql`
+ type Contact {
+ id: Int!
+ name: String!
+ email: String!
+ message: String!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ contacts: [Contact!]! @requireAuth
+ contact(id: Int!): Contact @requireAuth
+ }
+
+ input CreateContactInput {
+ name: String!
+ email: String!
+ message: String!
+ }
+
+ input UpdateContactInput {
+ name: String
+ email: String
+ message: String
+ }
+
+ type Mutation {
+ createContact(input: CreateContactInput!): Contact @skipAuth
+ updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth
+ deleteContact(id: Int!): Contact! @requireAuth(roles: ["ADMIN"])
+ }
+`
diff --git a/__fixtures__/esm-test-project/api/src/graphql/posts.sdl.ts b/__fixtures__/esm-test-project/api/src/graphql/posts.sdl.ts
new file mode 100644
index 0000000000..09cf9b2cc6
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/graphql/posts.sdl.ts
@@ -0,0 +1,33 @@
+export const schema = gql`
+ type Post {
+ id: Int!
+ title: String!
+ body: String!
+ authorId: Int!
+ author: User!
+ createdAt: DateTime!
+ }
+
+ type Query {
+ posts: [Post!]! @skipAuth
+ post(id: Int!): Post @skipAuth
+ }
+
+ input CreatePostInput {
+ title: String!
+ body: String!
+ authorId: Int!
+ }
+
+ input UpdatePostInput {
+ title: String
+ body: String
+ authorId: Int
+ }
+
+ type Mutation {
+ createPost(input: CreatePostInput!): Post! @requireAuth
+ updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
+ deletePost(id: Int!): Post! @requireAuth
+ }
+`
diff --git a/__fixtures__/esm-test-project/api/src/graphql/users.sdl.ts b/__fixtures__/esm-test-project/api/src/graphql/users.sdl.ts
new file mode 100644
index 0000000000..e2d1c0bed1
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/graphql/users.sdl.ts
@@ -0,0 +1,25 @@
+export const schema = gql`
+ type User {
+ id: Int!
+ email: String!
+ fullName: String!
+ roles: String
+ posts: [Post]!
+ }
+
+ type Query {
+ user(id: Int!): User @skipAuth
+ }
+
+ input CreateUserInput {
+ email: String!
+ fullName: String!
+ roles: String
+ }
+
+ input UpdateUserInput {
+ email: String
+ fullName: String
+ roles: String
+ }
+`
diff --git a/__fixtures__/esm-test-project/api/src/lib/auth.ts b/__fixtures__/esm-test-project/api/src/lib/auth.ts
new file mode 100644
index 0000000000..e7eafb1ef6
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/lib/auth.ts
@@ -0,0 +1,121 @@
+import type { Decoded } from '@cedarjs/api'
+import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server'
+
+import { db } from './db.js'
+
+/**
+ * The name of the cookie that dbAuth sets
+ *
+ * %port% will be replaced with the port the api server is running on.
+ * If you have multiple RW apps running on the same host, you'll need to
+ * make sure they all use unique cookie names
+ */
+export const cookieName = 'session_%port%'
+
+/**
+ * The session object sent in as the first argument to getCurrentUser() will
+ * have a single key `id` containing the unique ID of the logged in user
+ * (whatever field you set as `authFields.id` in your auth function config).
+ * You'll need to update the call to `db` below if you use a different model
+ * name or unique field name, for example:
+ *
+ * return await db.profile.findUnique({ where: { email: session.id } })
+ * ───┬─── ──┬──
+ * model accessor ─┘ unique id field name ─┘
+ *
+ * !! BEWARE !! Anything returned from this function will be available to the
+ * client--it becomes the content of `currentUser` on the web side (as well as
+ * `context.currentUser` on the api side). You should carefully add additional
+ * fields to the `select` object below once you've decided they are safe to be
+ * seen if someone were to open the Web Inspector in their browser.
+ */
+export const getCurrentUser = async (session: Decoded) => {
+ if (!session || typeof session.id !== 'number') {
+ throw new Error('Invalid session')
+ }
+
+ return await db.user.findUnique({
+ where: { id: session.id },
+ select: { id: true, roles: true, email: true },
+ })
+}
+
+/**
+ * The user is authenticated if there is a currentUser in the context
+ *
+ * @returns {boolean} - If the currentUser is authenticated
+ */
+export const isAuthenticated = (): boolean => {
+ return !!context.currentUser
+}
+
+/**
+ * When checking role membership, roles can be a single value, a list, or none.
+ * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
+ */
+type AllowedRoles = string | string[] | undefined
+
+/**
+ * Checks if the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
+ *
+ * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
+ * or when no roles are provided to check against. Otherwise returns false.
+ */
+export const hasRole = (roles: AllowedRoles): boolean => {
+ if (!isAuthenticated()) {
+ return false
+ }
+
+ const currentUserRoles = context.currentUser?.roles as string | string[]
+
+ if (typeof roles === 'string') {
+ if (typeof currentUserRoles === 'string') {
+ // roles to check is a string, currentUser.roles is a string
+ return currentUserRoles === roles
+ } else if (Array.isArray(currentUserRoles)) {
+ // roles to check is a string, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) => roles === allowedRole)
+ }
+ }
+
+ if (Array.isArray(roles)) {
+ if (Array.isArray(currentUserRoles)) {
+ // roles to check is an array, currentUser.roles is an array
+ return currentUserRoles?.some((allowedRole) =>
+ roles.includes(allowedRole)
+ )
+ } else if (typeof currentUserRoles === 'string') {
+ // roles to check is an array, currentUser.roles is a string
+ return roles.some((allowedRole) => currentUserRoles === allowedRole)
+ }
+ }
+
+ // roles not found
+ return false
+}
+
+/**
+ * Use requireAuth in your services to check that a user is logged in,
+ * whether or not they are assigned a role, and optionally raise an
+ * error if they're not.
+ *
+ * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
+ *
+ * @returns - If the currentUser is authenticated (and assigned one of the given roles)
+ *
+ * @throws {@link AuthenticationError} - If the currentUser is not authenticated
+ * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
+ *
+ * @see https://github.com/cedarjs/cedar/tree/main/packages/auth for examples
+ */
+export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
+ if (!isAuthenticated()) {
+ throw new AuthenticationError("You don't have permission to do that.")
+ }
+
+ if (roles && !hasRole(roles)) {
+ throw new ForbiddenError("You don't have access to do that.")
+ }
+}
diff --git a/__fixtures__/esm-test-project/api/src/lib/db.ts b/__fixtures__/esm-test-project/api/src/lib/db.ts
new file mode 100644
index 0000000000..b6fa93228f
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/lib/db.ts
@@ -0,0 +1,26 @@
+// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor
+// for options.
+
+import { PrismaClient } from '@prisma/client'
+
+import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'
+
+import { logger } from './logger.js'
+
+const prismaClient = new PrismaClient({
+ log: emitLogLevels(['info', 'warn', 'error']),
+})
+
+handlePrismaLogging({
+ db: prismaClient,
+ logger,
+ logLevels: ['info', 'warn', 'error'],
+})
+
+/**
+ * Global Prisma client extensions should be added here, as $extend
+ * returns a new instance.
+ * export const db = prismaClient.$extend(...)
+ * Add any .$on hooks before using $extend
+ */
+export const db = prismaClient
diff --git a/__fixtures__/esm-test-project/api/src/lib/logger.ts b/__fixtures__/esm-test-project/api/src/lib/logger.ts
new file mode 100644
index 0000000000..b5f38c1921
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/lib/logger.ts
@@ -0,0 +1,17 @@
+import { createLogger } from '@cedarjs/api/logger'
+
+/**
+ * Creates a logger with RedwoodLoggerOptions
+ *
+ * These extend and override default LoggerOptions,
+ * can define a destination like a file or other supported pino log transport stream,
+ * and sets whether or not to show the logger configuration settings (defaults to false)
+ *
+ * @param RedwoodLoggerOptions
+ *
+ * RedwoodLoggerOptions have
+ * @param {options} LoggerOptions - defines how to log, such as redaction and format
+ * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file
+ * @param {boolean} showConfig - whether to display logger configuration on initialization
+ */
+export const logger = createLogger({})
diff --git a/__fixtures__/esm-test-project/api/src/services/.keep b/__fixtures__/esm-test-project/api/src/services/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-test-project/api/src/services/contacts/contacts.scenarios.ts b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.scenarios.ts
new file mode 100644
index 0000000000..99271967d8
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Contact } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ contact: {
+ one: { data: { name: 'String', email: 'String', message: 'String' } },
+ two: { data: { name: 'String', email: 'String', message: 'String' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-test-project/api/src/services/contacts/contacts.test.ts b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.test.ts
new file mode 100644
index 0000000000..bec7102a29
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.test.ts
@@ -0,0 +1,59 @@
+import type { Contact } from '@prisma/client'
+
+import {
+ contacts,
+ contact,
+ createContact,
+ updateContact,
+ deleteContact,
+} from './contacts.js'
+import type { StandardScenario } from './contacts.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('contacts', () => {
+ scenario('returns all contacts', async (scenario: StandardScenario) => {
+ const result = await contacts()
+
+ expect(result.length).toEqual(Object.keys(scenario.contact).length)
+ })
+
+ scenario('returns a single contact', async (scenario: StandardScenario) => {
+ const result = await contact({ id: scenario.contact.one.id })
+
+ expect(result).toEqual(scenario.contact.one)
+ })
+
+ scenario('creates a contact', async () => {
+ const result = await createContact({
+ input: { name: 'String', email: 'String', message: 'String' },
+ })
+
+ expect(result.name).toEqual('String')
+ expect(result.email).toEqual('String')
+ expect(result.message).toEqual('String')
+ })
+
+ scenario('updates a contact', async (scenario: StandardScenario) => {
+ const original = (await contact({ id: scenario.contact.one.id })) as Contact
+ const result = await updateContact({
+ id: original.id,
+ input: { name: 'String2' },
+ })
+
+ expect(result.name).toEqual('String2')
+ })
+
+ scenario('deletes a contact', async (scenario: StandardScenario) => {
+ const original = (await deleteContact({
+ id: scenario.contact.one.id,
+ })) as Contact
+ const result = await contact({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/services/contacts/contacts.ts b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.ts
new file mode 100644
index 0000000000..88b001d48a
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/contacts/contacts.ts
@@ -0,0 +1,37 @@
+import type { QueryResolvers, MutationResolvers } from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const contacts: QueryResolvers['contacts'] = () => {
+ return db.contact.findMany()
+}
+
+export const contact: QueryResolvers['contact'] = ({ id }) => {
+ return db.contact.findUnique({
+ where: { id },
+ })
+}
+
+export const createContact: MutationResolvers['createContact'] = ({
+ input,
+}) => {
+ return db.contact.create({
+ data: input,
+ })
+}
+
+export const updateContact: MutationResolvers['updateContact'] = ({
+ id,
+ input,
+}) => {
+ return db.contact.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deleteContact: MutationResolvers['deleteContact'] = ({ id }) => {
+ return db.contact.delete({
+ where: { id },
+ })
+}
diff --git a/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.scenarios.ts b/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.scenarios.ts
new file mode 100644
index 0000000000..99271967d8
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.scenarios.ts
@@ -0,0 +1,12 @@
+import type { Prisma, Contact } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ contact: {
+ one: { data: { name: 'String', email: 'String', message: 'String' } },
+ two: { data: { name: 'String', email: 'String', message: 'String' } },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.test.ts b/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.test.ts
new file mode 100644
index 0000000000..ad9a697c07
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/contacts/describeContacts.test.ts
@@ -0,0 +1,57 @@
+import { db } from 'src/lib/db.js'
+
+import { contact, contacts, createContact } from './contacts.js'
+import type { StandardScenario } from './contacts.scenarios.js'
+
+/**
+ * Example test for describe scenario.
+ *
+ * Note that scenario tests need a matching [name].scenarios.ts file.
+ */
+
+describeScenario('contacts', (getScenario) => {
+ let scenario: StandardScenario
+
+ beforeEach(() => {
+ scenario = getScenario()
+ })
+
+ it('returns all contacts', async () => {
+ const result = await contacts()
+
+ expect(result.length).toEqual(Object.keys(scenario.contact).length)
+ })
+
+ it('returns a single contact', async () => {
+ const result = await contact({ id: scenario.contact.one.id })
+
+ expect(result).toEqual(scenario.contact.one)
+ })
+
+ it('creates a contact', async () => {
+ const result = await createContact({
+ input: {
+ name: 'Bazinga',
+ email: 'contact@describe.scenario',
+ message: 'Describe scenario works!',
+ },
+ })
+
+ expect(result.name).toEqual('Bazinga')
+ expect(result.email).toEqual('contact@describe.scenario')
+ expect(result.message).toEqual('Describe scenario works!')
+ })
+
+ it('Checking that describe scenario works', async () => {
+ // This test is dependent on the above test. If you used a normal scenario it would not work
+ const contactCreatedInAboveTest = await db.contact.findFirst({
+ where: {
+ email: 'contact@describe.scenario',
+ },
+ })
+
+ expect(contactCreatedInAboveTest.message).toEqual(
+ 'Describe scenario works!'
+ )
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/services/posts/posts.scenarios.ts b/__fixtures__/esm-test-project/api/src/services/posts/posts.scenarios.ts
new file mode 100644
index 0000000000..6890e49ad1
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/posts/posts.scenarios.ts
@@ -0,0 +1,38 @@
+import type { Prisma, Post } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ post: {
+ one: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String13',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ two: {
+ data: {
+ title: 'String',
+ body: 'String',
+ author: {
+ create: {
+ email: 'String27',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-test-project/api/src/services/posts/posts.test.ts b/__fixtures__/esm-test-project/api/src/services/posts/posts.test.ts
new file mode 100644
index 0000000000..b7c7572341
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/posts/posts.test.ts
@@ -0,0 +1,55 @@
+import type { Post } from '@prisma/client'
+
+import { posts, post, createPost, updatePost, deletePost } from './posts.js'
+import type { StandardScenario } from './posts.scenarios.js'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-services
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('posts', () => {
+ scenario('returns all posts', async (scenario: StandardScenario) => {
+ const result = await posts()
+
+ expect(result.length).toEqual(Object.keys(scenario.post).length)
+ })
+
+ scenario('returns a single post', async (scenario: StandardScenario) => {
+ const result = await post({ id: scenario.post.one.id })
+
+ expect(result).toEqual(scenario.post.one)
+ })
+
+ scenario('creates a post', async (scenario: StandardScenario) => {
+ const result = await createPost({
+ input: {
+ title: 'String',
+ body: 'String',
+ authorId: scenario.post.two.authorId,
+ },
+ })
+
+ expect(result.title).toEqual('String')
+ expect(result.body).toEqual('String')
+ expect(result.authorId).toEqual(scenario.post.two.authorId)
+ })
+
+ scenario('updates a post', async (scenario: StandardScenario) => {
+ const original = (await post({ id: scenario.post.one.id })) as Post
+ const result = await updatePost({
+ id: original.id,
+ input: { title: 'String2' },
+ })
+
+ expect(result.title).toEqual('String2')
+ })
+
+ scenario('deletes a post', async (scenario: StandardScenario) => {
+ const original = (await deletePost({ id: scenario.post.one.id })) as Post
+ const result = await post({ id: original.id })
+
+ expect(result).toEqual(null)
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/services/posts/posts.ts b/__fixtures__/esm-test-project/api/src/services/posts/posts.ts
new file mode 100644
index 0000000000..afbc87a734
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/posts/posts.ts
@@ -0,0 +1,42 @@
+import type {
+ QueryResolvers,
+ MutationResolvers,
+ PostRelationResolvers,
+} from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export const posts: QueryResolvers['posts'] = () => {
+ return db.post.findMany()
+}
+
+export const post: QueryResolvers['post'] = ({ id }) => {
+ return db.post.findUnique({
+ where: { id },
+ })
+}
+
+export const createPost: MutationResolvers['createPost'] = ({ input }) => {
+ return db.post.create({
+ data: input,
+ })
+}
+
+export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
+ return db.post.update({
+ data: input,
+ where: { id },
+ })
+}
+
+export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
+ return db.post.delete({
+ where: { id },
+ })
+}
+
+export const Post: PostRelationResolvers = {
+ author: (_obj, { root }) => {
+ return db.post.findUnique({ where: { id: root?.id } }).author()
+ },
+}
diff --git a/__fixtures__/esm-test-project/api/src/services/users/users.scenarios.ts b/__fixtures__/esm-test-project/api/src/services/users/users.scenarios.ts
new file mode 100644
index 0000000000..159e3cabd9
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/users/users.scenarios.ts
@@ -0,0 +1,26 @@
+import type { Prisma, User } from '@prisma/client'
+
+import type { ScenarioData } from '@cedarjs/testing/api'
+
+export const standard = defineScenario({
+ user: {
+ one: {
+ data: {
+ email: 'String9',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ two: {
+ data: {
+ email: 'String17',
+ hashedPassword: 'String',
+ fullName: 'String',
+ salt: 'String',
+ },
+ },
+ },
+})
+
+export type StandardScenario = ScenarioData
diff --git a/__fixtures__/esm-test-project/api/src/services/users/users.test.ts b/__fixtures__/esm-test-project/api/src/services/users/users.test.ts
new file mode 100644
index 0000000000..f754f23d1d
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/users/users.test.ts
@@ -0,0 +1,10 @@
+import { user } from './users.js'
+import type { StandardScenario } from './users.scenarios.js'
+
+describe('users', () => {
+ scenario('returns a single user', async (scenario: StandardScenario) => {
+ const result = await user({ id: scenario.user.one.id })
+
+ expect(result).toEqual(scenario.user.one)
+ })
+})
diff --git a/__fixtures__/esm-test-project/api/src/services/users/users.ts b/__fixtures__/esm-test-project/api/src/services/users/users.ts
new file mode 100644
index 0000000000..5aeb1f4e53
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/src/services/users/users.ts
@@ -0,0 +1,17 @@
+import type { QueryResolvers, UserRelationResolvers } from 'types/graphql.js'
+
+import { db } from 'src/lib/db.js'
+
+export {}
+
+export const user: QueryResolvers['user'] = ({ id }) => {
+ return db.user.findUnique({
+ where: { id },
+ })
+}
+
+export const User: UserRelationResolvers = {
+ posts: (_obj, { root }) => {
+ return db.user.findUnique({ where: { id: root?.id } }).posts()
+ },
+}
diff --git a/__fixtures__/esm-test-project/api/tsconfig.json b/__fixtures__/esm-test-project/api/tsconfig.json
new file mode 100644
index 0000000000..c9075d6fac
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "skipLibCheck": false,
+ "rootDirs": ["./src", "../.redwood/types/mirror/api/src"],
+ "paths": {
+ "src/*": ["./src/*", "../.redwood/types/mirror/api/src/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/api"]
+ },
+ "typeRoots": ["../node_modules/@types", "./node_modules/@types"],
+ "types": ["jest"],
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/api-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/esm-test-project/api/vitest-sort.config.ts b/__fixtures__/esm-test-project/api/vitest-sort.config.ts
new file mode 100644
index 0000000000..1e22e5e339
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/vitest-sort.config.ts
@@ -0,0 +1,26 @@
+import { defineConfig } from 'vitest/config'
+import { BaseSequencer } from 'vitest/node'
+import type { TestSpecification } from 'vitest/node'
+
+import { cedarVitestPreset } from '@cedarjs/vite/api'
+
+class SortSequencer extends BaseSequencer {
+ async sort(tests: TestSpecification[]) {
+ // This is currently setup to only test the db import tracking Cedar has
+ // in its vitest config for the api side
+ // Feel free to extend this if you need to test other features
+ return tests
+ .filter((test) => test.moduleId.endsWith('-db-import.test.ts'))
+ .sort()
+ }
+}
+
+export default defineConfig({
+ plugins: [cedarVitestPreset()],
+ test: {
+ globals: true,
+ sequence: {
+ sequencer: SortSequencer,
+ },
+ },
+})
diff --git a/__fixtures__/esm-test-project/api/vitest.config.ts b/__fixtures__/esm-test-project/api/vitest.config.ts
new file mode 100644
index 0000000000..f3de348f75
--- /dev/null
+++ b/__fixtures__/esm-test-project/api/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config'
+
+import { cedarVitestPreset } from '@cedarjs/vite/api'
+
+export default defineConfig({
+ plugins: [cedarVitestPreset()],
+ test: {
+ globals: true,
+ },
+})
diff --git a/__fixtures__/esm-test-project/graphql.config.cjs b/__fixtures__/esm-test-project/graphql.config.cjs
new file mode 100644
index 0000000000..d82bff34e0
--- /dev/null
+++ b/__fixtures__/esm-test-project/graphql.config.cjs
@@ -0,0 +1,11 @@
+// This file is used by the VSCode GraphQL extension
+
+const { getPaths } = require('@cedarjs/project-config')
+
+/** @type {import('graphql-config').IGraphQLConfig} */
+const config = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
+}
+
+module.exports = config
diff --git a/__fixtures__/esm-test-project/package.json b/__fixtures__/esm-test-project/package.json
new file mode 100644
index 0000000000..b6623fdc29
--- /dev/null
+++ b/__fixtures__/esm-test-project/package.json
@@ -0,0 +1,33 @@
+{
+ "private": true,
+ "type": "module",
+ "workspaces": {
+ "packages": [
+ "api",
+ "web"
+ ]
+ },
+ "devDependencies": {
+ "@cedarjs/core": "0.0.5",
+ "@cedarjs/project-config": "0.0.5",
+ "@cedarjs/testing": "0.0.5",
+ "vitest": "3.2.4",
+ "prettier-plugin-tailwindcss": "^0.5.12"
+ },
+ "eslintConfig": {
+ "extends": "@cedarjs/eslint-config",
+ "root": true
+ },
+ "engines": {
+ "node": "=20.x"
+ },
+ "prisma": {
+ "seed": "yarn rw exec seed"
+ },
+ "packageManager": "yarn@4.9.2",
+ "resolutions": {
+ "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
+ "react-is": "19.0.0-rc-f2df5694-20240916",
+ "vite": "5.4.16"
+ }
+}
\ No newline at end of file
diff --git a/__fixtures__/esm-test-project/prettier.config.cjs b/__fixtures__/esm-test-project/prettier.config.cjs
new file mode 100644
index 0000000000..9d81988854
--- /dev/null
+++ b/__fixtures__/esm-test-project/prettier.config.cjs
@@ -0,0 +1,20 @@
+// https://prettier.io/docs/en/options.html
+/** @type {import('prettier').RequiredOptions} */
+module.exports = {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ overrides: [
+ {
+ files: 'Routes.*',
+ options: {
+ printWidth: 999,
+ },
+ },
+ ],
+ tailwindConfig: './web/config/tailwind.config.cjs',
+ plugins: ['prettier-plugin-tailwindcss'],
+}
diff --git a/__fixtures__/esm-test-project/redwood.toml b/__fixtures__/esm-test-project/redwood.toml
new file mode 100644
index 0000000000..3f927164d5
--- /dev/null
+++ b/__fixtures__/esm-test-project/redwood.toml
@@ -0,0 +1,21 @@
+# This file contains the configuration settings for your Redwood app.
+# This file is also what makes your Redwood app a Redwood app.
+# If you remove it and try to run `yarn rw dev`, you'll get an error.
+#
+# For the full list of options, see the "App Configuration: redwood.toml" doc:
+# https://redwoodjs.com/docs/app-configuration-redwood-toml
+
+[web]
+ title = "Cedar App"
+ port = "${WEB_DEV_PORT:8910}"
+ apiUrl = "/.redwood/functions"
+ includeEnvironmentVariables = [
+ # Add any ENV vars that should be available to the web side to this array
+ # See https://redwoodjs.com/docs/environment-variables#web
+ ]
+[api]
+ port = "${API_DEV_PORT:8911}"
+[browser]
+ open = true
+[notifications]
+ versionUpdates = ["latest"]
diff --git a/__fixtures__/esm-test-project/scripts/.keep b/__fixtures__/esm-test-project/scripts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-test-project/scripts/i/am/nested.ts b/__fixtures__/esm-test-project/scripts/i/am/nested.ts
new file mode 100644
index 0000000000..6037354e1b
--- /dev/null
+++ b/__fixtures__/esm-test-project/scripts/i/am/nested.ts
@@ -0,0 +1,15 @@
+// Append api/* to import from api and web/* to import from web
+
+// To access your database uncomment the line below
+// import { db } from 'api/src/lib/db'
+
+interface Args {
+ _: string[]
+ [key: string]: unknown
+}
+
+export default async ({ args }: Args) => {
+ // Your script here...
+ console.log(':: Executing script with args ::')
+ console.log(args)
+}
diff --git a/__fixtures__/esm-test-project/scripts/one/two/myNestedScript.ts b/__fixtures__/esm-test-project/scripts/one/two/myNestedScript.ts
new file mode 100644
index 0000000000..d8e6c4138e
--- /dev/null
+++ b/__fixtures__/esm-test-project/scripts/one/two/myNestedScript.ts
@@ -0,0 +1,3 @@
+export default async () => {
+ console.log('Hello from myNestedScript.ts')
+}
diff --git a/__fixtures__/esm-test-project/scripts/seed.ts b/__fixtures__/esm-test-project/scripts/seed.ts
new file mode 100644
index 0000000000..cdee167538
--- /dev/null
+++ b/__fixtures__/esm-test-project/scripts/seed.ts
@@ -0,0 +1,88 @@
+import { db } from 'api/src/lib/db.js'
+
+// Manually apply seeds via the `yarn rw prisma db seed` command.
+//
+// Seeds automatically run the first time you run the `yarn rw prisma migrate dev`
+// command and every time you run the `yarn rw prisma migrate reset` command.
+//
+// See https://redwoodjs.com/docs/database-seeds for more info
+
+export default async () => {
+ try {
+ const users = [
+ {
+ id: 1,
+ email: 'user.one@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User One',
+ salt: 'fake_salt',
+ },
+ {
+ id: 2,
+ email: 'user.two@example.com',
+ hashedPassword: 'fake_hash',
+ fullName: 'User Two',
+ salt: 'fake_salt',
+ },
+ ]
+
+ if ((await db.user.count()) === 0) {
+ await Promise.all(users.map((user) => db.user.create({ data: user })))
+ } else {
+ console.log('Users already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ const posts = [
+ {
+ title: 'Welcome to the blog!',
+ body: "I'm baby single- origin coffee kickstarter lo - fi paleo skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.",
+ authorId: 1,
+ },
+ {
+ title: 'A little more about me',
+ body: "Raclette shoreditch before they sold out lyft. Ethical bicycle rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage etsy pitchfork sriracha tumeric bushwick.",
+ authorId: 1,
+ },
+ {
+ title: 'What is the meaning of life?',
+ body: 'Meh waistcoat succulents umami asymmetrical, hoodie post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.',
+ authorId: 2,
+ },
+ ]
+
+ if ((await db.post.count()) === 0) {
+ await Promise.all(
+ posts.map(async (post) => {
+ const newPost = await db.post.create({ data: post })
+
+ console.log(newPost)
+ })
+ )
+ } else {
+ console.log('Posts already seeded')
+ }
+ } catch (error) {
+ console.error(error)
+ }
+
+ try {
+ // Create your database records here! For example, seed some users:
+ //
+ // const users = [
+ // { name: 'Alice', email: 'alice@cedarjs.com' },
+ // { name: 'Bob', email: 'bob@cedarjs.com' },
+ // ]
+ //
+ // await db.user.createMany({ data: users })
+
+ console.info(
+ '\n No seed data, skipping. See scripts/seed.ts to start seeding your database!\n'
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/__fixtures__/esm-test-project/scripts/tsconfig.json b/__fixtures__/esm-test-project/scripts/tsconfig.json
new file mode 100644
index 0000000000..5690eec16c
--- /dev/null
+++ b/__fixtures__/esm-test-project/scripts/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "paths": {
+ "$api/*": ["../api/*"],
+ "api/*": ["../api/*"],
+ "$api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "$web/*": ["../web/*"],
+ "web/*": ["../web/*"],
+ "$web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "types/*": ["../types/*", "../web/types/*", "../api/types/*"]
+ },
+ "typeRoots": ["../node_modules/@types"],
+ "jsx": "preserve"
+ },
+ "include": [
+ ".",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types"
+ ]
+}
diff --git a/__fixtures__/esm-test-project/vitest.config.ts b/__fixtures__/esm-test-project/vitest.config.ts
new file mode 100644
index 0000000000..6c64e7920b
--- /dev/null
+++ b/__fixtures__/esm-test-project/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: ['/{*,!(node_modules)/**/}/vite?(st).config.{js,ts}'],
+ },
+})
diff --git a/__fixtures__/esm-test-project/web/config/postcss.config.cjs b/__fixtures__/esm-test-project/web/config/postcss.config.cjs
new file mode 100644
index 0000000000..f6d96e144e
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/config/postcss.config.cjs
@@ -0,0 +1,9 @@
+const path = require('path')
+
+module.exports = {
+ plugins: [
+ require('tailwindcss/nesting'),
+ require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.cjs')),
+ require('autoprefixer'),
+ ],
+}
diff --git a/__fixtures__/esm-test-project/web/config/tailwind.config.cjs b/__fixtures__/esm-test-project/web/config/tailwind.config.cjs
new file mode 100644
index 0000000000..613a94fcab
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/config/tailwind.config.cjs
@@ -0,0 +1,10 @@
+const { join } = require('node:path')
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [join(__dirname, '../src/**/*.{js,jsx,ts,tsx}')],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/__fixtures__/esm-test-project/web/package.json b/__fixtures__/esm-test-project/web/package.json
new file mode 100644
index 0000000000..63050afa37
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "web",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "browserslist": {
+ "development": [
+ "last 1 version"
+ ],
+ "production": [
+ "defaults"
+ ]
+ },
+ "dependencies": {
+ "@cedarjs/auth-dbauth-web": "0.0.5",
+ "@cedarjs/forms": "0.0.5",
+ "@cedarjs/router": "0.0.5",
+ "@cedarjs/web": "0.0.5",
+ "humanize-string": "2.1.0",
+ "react": "19.0.0-rc-f2df5694-20240916",
+ "react-dom": "19.0.0-rc-f2df5694-20240916"
+ },
+ "devDependencies": {
+ "@cedarjs/vite": "0.0.5",
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19",
+ "autoprefixer": "^10.4.21",
+ "postcss": "^8.5.6",
+ "postcss-loader": "^8.1.1",
+ "tailwindcss": "^3.4.17"
+ }
+}
diff --git a/__fixtures__/esm-test-project/web/public/README.md b/__fixtures__/esm-test-project/web/public/README.md
new file mode 100644
index 0000000000..1b09bf8361
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/public/README.md
@@ -0,0 +1,43 @@
+# Static Assets
+
+Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`.
+
+> Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes.
+
+### Example Use
+
+A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
+
+```
+
+```
+
+and
+
+```
+ alt="Logo" />
+```
+
+## Best Practices
+
+Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc.
+
+In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash.
+
+### Example Asset Import with Vite
+
+Instead of handling our logo image as a static file per the example above, we can do the following:
+
+```
+import React from "react"
+import logo from "./my-logo.jpg"
+
+
+function Header() {
+ return
+}
+
+export default Header
+```
+
+See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html)
diff --git a/__fixtures__/esm-test-project/web/public/favicon.png b/__fixtures__/esm-test-project/web/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+
+
+ {children}
+
+
+
+
+)
+
+export default App
diff --git a/__fixtures__/esm-test-project/web/src/Redwood.stories.mdx b/__fixtures__/esm-test-project/web/src/Redwood.stories.mdx
new file mode 100644
index 0000000000..d0ef9e47cf
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/Redwood.stories.mdx
@@ -0,0 +1,18 @@
+import { Meta } from '@storybook/addon-docs'
+
+
+
+
+
+
Redwood
+
+
+_by Tom Preston-Werner, Peter Pistorius, Rob Cameron, David Price, and more than
+250 amazing contributors (see end of file for a full list)._
+
+**Redwood is an opinionated, full-stack, JavaScript/TypeScript web application
+framework designed to keep you moving fast as your app grows from side project
+to startup.**
diff --git a/__fixtures__/esm-test-project/web/src/Routes.tsx b/__fixtures__/esm-test-project/web/src/Routes.tsx
new file mode 100644
index 0000000000..1894deaa0c
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/Routes.tsx
@@ -0,0 +1,53 @@
+// In this file, all Page components from 'src/pages` are auto-imported. Nested
+// directories are supported, and should be uppercase. Each subdirectory will be
+// prepended onto the component name.
+//
+// Examples:
+//
+// 'src/pages/HomePage/HomePage.js' -> HomePage
+// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
+
+import { Router, Route, Private, Set } from '@cedarjs/router'
+
+import BlogLayout from 'src/layouts/BlogLayout'
+import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
+import HomePage from 'src/pages/HomePage'
+
+import { useAuth } from './auth.js'
+
+const Routes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Routes
diff --git a/__fixtures__/esm-test-project/web/src/auth.ts b/__fixtures__/esm-test-project/web/src/auth.ts
new file mode 100644
index 0000000000..ca02125c24
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/auth.ts
@@ -0,0 +1,5 @@
+import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/__fixtures__/esm-test-project/web/src/components/.keep b/__fixtures__/esm-test-project/web/src/components/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-test-project/web/src/components/Author/Author.stories.tsx b/__fixtures__/esm-test-project/web/src/components/Author/Author.stories.tsx
new file mode 100644
index 0000000000..29dbcf59fa
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Author/Author.stories.tsx
@@ -0,0 +1,35 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import Author from './Author'
+
+const meta: Meta = {
+ component: Author,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+const author = {
+ email: 'story.user@email.com',
+ fullName: 'Story User',
+}
+
+export const Primary: Story = {
+ render: () => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Author/Author.test.tsx b/__fixtures__/esm-test-project/web/src/components/Author/Author.test.tsx
new file mode 100644
index 0000000000..09e58d145e
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Author/Author.test.tsx
@@ -0,0 +1,19 @@
+import { render } from '@cedarjs/testing/web'
+
+import Author from './Author'
+
+const author = {
+ email: 'test.user@email.com',
+ fullName: 'Test User',
+}
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('Author', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/Author/Author.tsx b/__fixtures__/esm-test-project/web/src/components/Author/Author.tsx
new file mode 100644
index 0000000000..f59f8b98f1
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Author/Author.tsx
@@ -0,0 +1,16 @@
+interface Props {
+ author: {
+ email: string
+ fullName: string
+ }
+}
+
+const Author = ({ author }: Props) => {
+ return (
+
+ {author.fullName} ({author.email})
+
+ )
+}
+
+export default Author
diff --git a/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
new file mode 100644
index 0000000000..5dd8454b3d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts
@@ -0,0 +1,9 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ author: {
+ __typename: 'User' as const,
+ id: 42,
+ email: 'fortytwo@42.com',
+ fullName: 'Forty Two',
+ },
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
new file mode 100644
index 0000000000..fd0a2b7337
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/AuthorCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
new file mode 100644
index 0000000000..4fbec70cb1
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './AuthorCell'
+import { standard } from './AuthorCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('AuthorCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.tsx b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.tsx
new file mode 100644
index 0000000000..95180b96f6
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/AuthorCell/AuthorCell.tsx
@@ -0,0 +1,42 @@
+import type {
+ FindAuthorQuery,
+ FindAuthorQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Author from 'src/components/Author'
+
+export const QUERY: TypedDocumentNode<
+ FindAuthorQuery,
+ FindAuthorQueryVariables
+> = gql`
+ query FindAuthorQuery($id: Int!) {
+ author: user(id: $id) {
+ email
+ fullName
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ author,
+}: CellSuccessProps) => (
+
+
+
+)
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.stories.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
new file mode 100644
index 0000000000..4c4daef53f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.stories.tsx
@@ -0,0 +1,26 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogPost from './BlogPost'
+
+const meta: Meta = {
+ component: BlogPost,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.test.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.test.tsx
new file mode 100644
index 0000000000..c30451e8ad
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogPost from './BlogPost'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('BlogPost', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.tsx
new file mode 100644
index 0000000000..e13a2032e7
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPost/BlogPost.tsx
@@ -0,0 +1,41 @@
+import { FindBlogPostQuery } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+
+import Author from 'src/components/Author'
+
+interface Props extends FindBlogPostQuery {}
+
+const BlogPost = ({ blogPost }: Props) => {
+ return (
+
+ {blogPost && (
+ <>
+
+
+ {new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(blogPost.createdAt))}{' '}
+ - By:
+
+
+
+ {blogPost.title}
+
+
+
+
+ {blogPost.body}
+
+ >
+ )}
+
+ )
+}
+
+export default BlogPost
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
new file mode 100644
index 0000000000..e805ec44db
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts
@@ -0,0 +1,17 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPost: {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
new file mode 100644
index 0000000000..361d13a911
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/BlogPostCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
new file mode 100644
index 0000000000..9df5744f80
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './BlogPostCell'
+import { standard } from './BlogPostCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('BlogPostCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
new file mode 100644
index 0000000000..d02ba269df
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx
@@ -0,0 +1,46 @@
+import type {
+ FindBlogPostQuery,
+ FindBlogPostQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import BlogPost from 'src/components/BlogPost'
+
+export const QUERY: TypedDocumentNode<
+ FindBlogPostQuery,
+ FindBlogPostQueryVariables
+> = gql`
+ query FindBlogPostQuery($id: Int!) {
+ blogPost: post(id: $id) {
+ id
+ title
+ body
+ author {
+ email
+ fullName
+ }
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ blogPost,
+}: CellSuccessProps) => (
+
+)
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
new file mode 100644
index 0000000000..b1569845f5
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts
@@ -0,0 +1,47 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ blogPosts: [
+ {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ {
+ __typename: 'Post' as const,
+ id: 43,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ {
+ __typename: 'Post' as const,
+ id: 44,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 5,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'five@5.com',
+ fullName: 'Five Lastname',
+ },
+ },
+ ],
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
new file mode 100644
index 0000000000..bbd0cb8b0f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/BlogPostsCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
new file mode 100644
index 0000000000..5e7a947c9d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx
@@ -0,0 +1,42 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './BlogPostsCell'
+import { standard } from './BlogPostsCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('BlogPostsCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
new file mode 100644
index 0000000000..3f77a396ed
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx
@@ -0,0 +1,45 @@
+import type { BlogPostsQuery, BlogPostsQueryVariables } from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import BlogPost from 'src/components/BlogPost'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query BlogPostsQuery {
+ blogPosts: posts {
+ id
+ title
+ body
+ author {
+ email
+ fullName
+ }
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ blogPosts,
+}: CellSuccessProps) => (
+
+ {blogPosts.map((post) => (
+
+ ))}
+
+)
diff --git a/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
new file mode 100644
index 0000000000..b6855a5b07
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx
@@ -0,0 +1,26 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/7/writing-stories/args
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ClassWithClassField from './ClassWithClassField'
+
+const meta: Meta = {
+ component: ClassWithClassField,
+ tags: ['autodocs'],
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
new file mode 100644
index 0000000000..0f342ba74b
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import ClassWithClassField from './ClassWithClassField'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('ClassWithClassField', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
new file mode 100644
index 0000000000..10cf41bad5
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx
@@ -0,0 +1,12 @@
+class Bar {}
+
+class Foo {
+ // Without the correct babel plugins this will throw an error
+ public bar = new Bar()
+}
+
+const ClassWithClassField = () => {
+ return {new Foo().bar.toString()}
+}
+
+export default ClassWithClassField
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/Contact/Contact.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/Contact/Contact.tsx
new file mode 100644
index 0000000000..f90bff1d17
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/Contact/Contact.tsx
@@ -0,0 +1,98 @@
+import type {
+ DeleteContactMutation,
+ DeleteContactMutationVariables,
+ FindContactById,
+} from 'types/graphql.js'
+
+import { Link, routes, navigate } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { timeTag } from 'src/lib/formatters.js'
+
+const DELETE_CONTACT_MUTATION: TypedDocumentNode<
+ DeleteContactMutation,
+ DeleteContactMutationVariables
+> = gql`
+ mutation DeleteContactMutation($id: Int!) {
+ deleteContact(id: $id) {
+ id
+ }
+ }
+`
+
+interface Props {
+ contact: NonNullable
+}
+
+const Contact = ({ contact }: Props) => {
+ const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, {
+ onCompleted: () => {
+ toast.success('Contact deleted')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onDeleteClick = (id: DeleteContactMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete contact ' + id + '?')) {
+ deleteContact({ variables: { id } })
+ }
+ }
+
+ return (
+ <>
+
+
+
+ Contact {contact.id} Detail
+
+
+
+
+
+ Id
+ {contact.id}
+
+
+ Name
+ {contact.name}
+
+
+ Email
+ {contact.email}
+
+
+ Message
+ {contact.message}
+
+
+ Created at
+ {timeTag(contact.createdAt)}
+
+
+
+
+
+
+ Edit
+
+ onDeleteClick(contact.id)}
+ >
+ Delete
+
+
+ >
+ )
+}
+
+export default Contact
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
new file mode 100644
index 0000000000..c3470c7360
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx
@@ -0,0 +1,43 @@
+import type {
+ FindContactById,
+ FindContactByIdVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Contact from 'src/components/Contact/Contact'
+
+export const QUERY: TypedDocumentNode<
+ FindContactById,
+ FindContactByIdVariables
+> = gql`
+ query FindContactById($id: Int!) {
+ contact: contact(id: $id) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Contact not found
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ contact,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
new file mode 100644
index 0000000000..024b762b2d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx
@@ -0,0 +1,101 @@
+import type { EditContactById, UpdateContactInput } from 'types/graphql.js'
+
+import type { RWGqlError } from '@cedarjs/forms'
+import {
+ Form,
+ FormError,
+ FieldError,
+ Label,
+ TextField,
+ Submit,
+} from '@cedarjs/forms'
+
+type FormContact = NonNullable
+
+interface ContactFormProps {
+ contact?: EditContactById['contact']
+ onSave: (data: UpdateContactInput, id?: FormContact['id']) => void
+ error: RWGqlError
+ loading: boolean
+}
+
+const ContactForm = (props: ContactFormProps) => {
+ const onSubmit = (data: FormContact) => {
+ props.onSave(data, props?.contact?.id)
+ }
+
+ return (
+
+ )
+}
+
+export default ContactForm
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/Contacts/Contacts.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/Contacts/Contacts.tsx
new file mode 100644
index 0000000000..30528f3f3f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/Contacts/Contacts.tsx
@@ -0,0 +1,102 @@
+import type {
+ DeleteContactMutation,
+ DeleteContactMutationVariables,
+ FindContacts,
+} from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { QUERY } from 'src/components/Contact/ContactsCell'
+import { timeTag, truncate } from 'src/lib/formatters.js'
+
+const DELETE_CONTACT_MUTATION: TypedDocumentNode<
+ DeleteContactMutation,
+ DeleteContactMutationVariables
+> = gql`
+ mutation DeleteContactMutation($id: Int!) {
+ deleteContact(id: $id) {
+ id
+ }
+ }
+`
+
+const ContactsList = ({ contacts }: FindContacts) => {
+ const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, {
+ onCompleted: () => {
+ toast.success('Contact deleted')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ // This refetches the query on the list page. Read more about other ways to
+ // update the cache over here:
+ // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
+ refetchQueries: [{ query: QUERY }],
+ awaitRefetchQueries: true,
+ })
+
+ const onDeleteClick = (id: DeleteContactMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete contact ' + id + '?')) {
+ deleteContact({ variables: { id } })
+ }
+ }
+
+ return (
+
+
+
+
+ Id
+ Name
+ Email
+ Message
+ Created at
+
+
+
+
+ {contacts.map((contact) => (
+
+ {truncate(contact.id)}
+ {truncate(contact.name)}
+ {truncate(contact.email)}
+ {truncate(contact.message)}
+ {timeTag(contact.createdAt)}
+
+
+
+ Show
+
+
+ Edit
+
+ onDeleteClick(contact.id)}
+ >
+ Delete
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export default ContactsList
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
new file mode 100644
index 0000000000..e64d6a8e96
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx
@@ -0,0 +1,46 @@
+import type { FindContacts, FindContactsVariables } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Contacts from 'src/components/Contact/Contacts'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query FindContacts {
+ contacts {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => {
+ return (
+
+ No contacts yet.{' '}
+
+ Create one?
+
+
+ )
+}
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ contacts,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
new file mode 100644
index 0000000000..622e63f686
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx
@@ -0,0 +1,89 @@
+import type {
+ EditContactById,
+ UpdateContactInput,
+ UpdateContactMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+import { useMutation } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import ContactForm from 'src/components/Contact/ContactForm'
+
+export const QUERY: TypedDocumentNode = gql`
+ query EditContactById($id: Int!) {
+ contact: contact(id: $id) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+const UPDATE_CONTACT_MUTATION: TypedDocumentNode<
+ EditContactById,
+ UpdateContactMutationVariables
+> = gql`
+ mutation UpdateContactMutation($id: Int!, $input: UpdateContactInput!) {
+ updateContact(id: $id, input: $input) {
+ id
+ name
+ email
+ message
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({ contact }: CellSuccessProps) => {
+ const [updateContact, { loading, error }] = useMutation(
+ UPDATE_CONTACT_MUTATION,
+ {
+ onCompleted: () => {
+ toast.success('Contact updated')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+
+ const onSave = (
+ input: UpdateContactInput,
+ id: EditContactById['contact']['id']
+ ) => {
+ updateContact({ variables: { id, input } })
+ }
+
+ return (
+
+
+
+ Edit Contact {contact?.id}
+
+
+
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Contact/NewContact/NewContact.tsx b/__fixtures__/esm-test-project/web/src/components/Contact/NewContact/NewContact.tsx
new file mode 100644
index 0000000000..2cb66a3c30
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Contact/NewContact/NewContact.tsx
@@ -0,0 +1,55 @@
+import type {
+ CreateContactMutation,
+ CreateContactInput,
+ CreateContactMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import ContactForm from 'src/components/Contact/ContactForm'
+
+const CREATE_CONTACT_MUTATION: TypedDocumentNode<
+ CreateContactMutation,
+ CreateContactMutationVariables
+> = gql`
+ mutation CreateContactMutation($input: CreateContactInput!) {
+ createContact(input: $input) {
+ id
+ }
+ }
+`
+
+const NewContact = () => {
+ const [createContact, { loading, error }] = useMutation(
+ CREATE_CONTACT_MUTATION,
+ {
+ onCompleted: () => {
+ toast.success('Contact created')
+ navigate(routes.contacts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+
+ const onSave = (input: CreateContactInput) => {
+ createContact({ variables: { input } })
+ }
+
+ return (
+
+ )
+}
+
+export default NewContact
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx b/__fixtures__/esm-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
new file mode 100644
index 0000000000..53a0bd1079
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx
@@ -0,0 +1,78 @@
+import type {
+ EditPostById,
+ UpdatePostInput,
+ UpdatePostMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+import { useMutation } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import PostForm from 'src/components/Post/PostForm'
+
+export const QUERY: TypedDocumentNode = gql`
+ query EditPostById($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+const UPDATE_POST_MUTATION: TypedDocumentNode<
+ EditPostById,
+ UpdatePostMutationVariables
+> = gql`
+ mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) {
+ updatePost(id: $id, input: $input) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({ post }: CellSuccessProps) => {
+ const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post updated')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSave = (input: UpdatePostInput, id: EditPostById['post']['id']) => {
+ updatePost({ variables: { id, input } })
+ }
+
+ return (
+
+
+
+ Edit Post {post?.id}
+
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/NewPost/NewPost.tsx b/__fixtures__/esm-test-project/web/src/components/Post/NewPost/NewPost.tsx
new file mode 100644
index 0000000000..4a3a8b91bd
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/NewPost/NewPost.tsx
@@ -0,0 +1,52 @@
+import type {
+ CreatePostMutation,
+ CreatePostInput,
+ CreatePostMutationVariables,
+} from 'types/graphql.js'
+
+import { navigate, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import PostForm from 'src/components/Post/PostForm'
+
+const CREATE_POST_MUTATION: TypedDocumentNode<
+ CreatePostMutation,
+ CreatePostMutationVariables
+> = gql`
+ mutation CreatePostMutation($input: CreatePostInput!) {
+ createPost(input: $input) {
+ id
+ }
+ }
+`
+
+const NewPost = () => {
+ const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post created')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSave = (input: CreatePostInput) => {
+ createPost({ variables: { input } })
+ }
+
+ return (
+
+ )
+}
+
+export default NewPost
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/Post/Post.tsx b/__fixtures__/esm-test-project/web/src/components/Post/Post/Post.tsx
new file mode 100644
index 0000000000..dfbc5a413c
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/Post/Post.tsx
@@ -0,0 +1,98 @@
+import type {
+ DeletePostMutation,
+ DeletePostMutationVariables,
+ FindPostById,
+} from 'types/graphql.js'
+
+import { Link, routes, navigate } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { timeTag } from 'src/lib/formatters.js'
+
+const DELETE_POST_MUTATION: TypedDocumentNode<
+ DeletePostMutation,
+ DeletePostMutationVariables
+> = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+interface Props {
+ post: NonNullable
+}
+
+const Post = ({ post }: Props) => {
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post deleted')
+ navigate(routes.posts())
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id } })
+ }
+ }
+
+ return (
+ <>
+
+
+
+ Post {post.id} Detail
+
+
+
+
+
+ Id
+ {post.id}
+
+
+ Title
+ {post.title}
+
+
+ Body
+ {post.body}
+
+
+ Author id
+ {post.authorId}
+
+
+ Created at
+ {timeTag(post.createdAt)}
+
+
+
+
+
+
+ Edit
+
+ onDeleteClick(post.id)}
+ >
+ Delete
+
+
+ >
+ )
+}
+
+export default Post
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/PostCell/PostCell.tsx b/__fixtures__/esm-test-project/web/src/components/Post/PostCell/PostCell.tsx
new file mode 100644
index 0000000000..f156749b0d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/PostCell/PostCell.tsx
@@ -0,0 +1,36 @@
+import type { FindPostById, FindPostByIdVariables } from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Post from 'src/components/Post/Post'
+
+export const QUERY: TypedDocumentNode =
+ gql`
+ query FindPostById($id: Int!) {
+ post: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+ `
+
+export const Loading = () => Loading...
+
+export const Empty = () => Post not found
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ post,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/PostForm/PostForm.tsx b/__fixtures__/esm-test-project/web/src/components/Post/PostForm/PostForm.tsx
new file mode 100644
index 0000000000..4a17df0ca1
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/PostForm/PostForm.tsx
@@ -0,0 +1,102 @@
+import type { EditPostById, UpdatePostInput } from 'types/graphql.js'
+
+import type { RWGqlError } from '@cedarjs/forms'
+import {
+ Form,
+ FormError,
+ FieldError,
+ Label,
+ TextField,
+ NumberField,
+ Submit,
+} from '@cedarjs/forms'
+
+type FormPost = NonNullable
+
+interface PostFormProps {
+ post?: EditPostById['post']
+ onSave: (data: UpdatePostInput, id?: FormPost['id']) => void
+ error: RWGqlError
+ loading: boolean
+}
+
+const PostForm = (props: PostFormProps) => {
+ const onSubmit = (data: FormPost) => {
+ props.onSave(data, props?.post?.id)
+ }
+
+ return (
+
+ )
+}
+
+export default PostForm
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/Posts/Posts.tsx b/__fixtures__/esm-test-project/web/src/components/Post/Posts/Posts.tsx
new file mode 100644
index 0000000000..edbf98fe98
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/Posts/Posts.tsx
@@ -0,0 +1,102 @@
+import type {
+ DeletePostMutation,
+ DeletePostMutationVariables,
+ FindPosts,
+} from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import type { TypedDocumentNode } from '@cedarjs/web'
+import { toast } from '@cedarjs/web/toast'
+
+import { QUERY } from 'src/components/Post/PostsCell'
+import { timeTag, truncate } from 'src/lib/formatters.js'
+
+const DELETE_POST_MUTATION: TypedDocumentNode<
+ DeletePostMutation,
+ DeletePostMutationVariables
+> = gql`
+ mutation DeletePostMutation($id: Int!) {
+ deletePost(id: $id) {
+ id
+ }
+ }
+`
+
+const PostsList = ({ posts }: FindPosts) => {
+ const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+ onCompleted: () => {
+ toast.success('Post deleted')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ // This refetches the query on the list page. Read more about other ways to
+ // update the cache over here:
+ // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
+ refetchQueries: [{ query: QUERY }],
+ awaitRefetchQueries: true,
+ })
+
+ const onDeleteClick = (id: DeletePostMutationVariables['id']) => {
+ if (confirm('Are you sure you want to delete post ' + id + '?')) {
+ deletePost({ variables: { id } })
+ }
+ }
+
+ return (
+
+
+
+
+ Id
+ Title
+ Body
+ Author id
+ Created at
+
+
+
+
+ {posts.map((post) => (
+
+ {truncate(post.id)}
+ {truncate(post.title)}
+ {truncate(post.body)}
+ {truncate(post.authorId)}
+ {timeTag(post.createdAt)}
+
+
+
+ Show
+
+
+ Edit
+
+ onDeleteClick(post.id)}
+ >
+ Delete
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export default PostsList
diff --git a/__fixtures__/esm-test-project/web/src/components/Post/PostsCell/PostsCell.tsx b/__fixtures__/esm-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
new file mode 100644
index 0000000000..8a3f465701
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/Post/PostsCell/PostsCell.tsx
@@ -0,0 +1,45 @@
+import type { FindPosts, FindPostsVariables } from 'types/graphql.js'
+
+import { Link, routes } from '@cedarjs/router'
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import Posts from 'src/components/Post/Posts'
+
+export const QUERY: TypedDocumentNode = gql`
+ query FindPosts {
+ posts {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => {
+ return (
+
+ No posts yet.{' '}
+
+ Create one?
+
+
+ )
+}
+
+export const Failure = ({ error }: CellFailureProps) => (
+ {error?.message}
+)
+
+export const Success = ({
+ posts,
+}: CellSuccessProps) => {
+ return
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
new file mode 100644
index 0000000000..cf02b60610
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts
@@ -0,0 +1,17 @@
+// Define your own mock data here:
+export const standard = (/* vars, { ctx, req } */) => ({
+ waterfallBlogPost: {
+ __typename: 'Post' as const,
+ id: 42,
+ title: 'Mocked title',
+ body: 'Mocked body',
+ createdAt: '2022-01-17T13:57:51.607Z',
+ authorId: 7,
+
+ author: {
+ __typename: 'User' as const,
+ email: 'se7en@7.com',
+ fullName: 'Se7en Lastname',
+ },
+ },
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
new file mode 100644
index 0000000000..7109babeb3
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell'
+import { standard } from './WaterfallBlogPostCell.mock'
+
+const meta: Meta = {
+ title: 'Cells/WaterfallBlogPostCell',
+ tags: ['autodocs'],
+}
+
+export default meta
+
+export const loading: StoryObj = {
+ render: () => {
+ return Loading ? : <>>
+ },
+}
+
+export const empty: StoryObj = {
+ render: () => {
+ return Empty ? : <>>
+ },
+}
+
+export const failure: StoryObj = {
+ render: (args) => {
+ return Failure ? : <>>
+ },
+}
+
+export const success: StoryObj = {
+ render: (args) => {
+ return Success ? : <>>
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
new file mode 100644
index 0000000000..6701312f2c
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx
@@ -0,0 +1,44 @@
+import { render } from '@cedarjs/testing/web'
+
+import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell'
+import { standard } from './WaterfallBlogPostCell.mock'
+
+// Generated boilerplate tests do not account for all circumstances
+// and can fail without adjustments, e.g. Float and DateTime types.
+// Please refer to the RedwoodJS Testing Docs:
+// https://redwoodjs.com/docs/testing#testing-cells
+// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
+
+describe('WaterfallBlogPostCell', () => {
+ it('renders Loading successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Empty successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ it('renders Failure successfully', async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ // When you're ready to test the actual output of your component render
+ // you could test that, for example, certain text is present:
+ //
+ // 1. import { screen } from '@cedarjs/testing/web'
+ // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
+
+ it('renders Success successfully', async () => {
+ expect(() => {
+ render(
+
+ )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
new file mode 100644
index 0000000000..075ca427a6
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx
@@ -0,0 +1,67 @@
+import type {
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables,
+} from 'types/graphql.js'
+
+import type {
+ CellSuccessProps,
+ CellFailureProps,
+ TypedDocumentNode,
+} from '@cedarjs/web'
+
+import AuthorCell from 'src/components/AuthorCell'
+
+export const QUERY: TypedDocumentNode<
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables
+> = gql`
+ query FindWaterfallBlogPostQuery($id: Int!) {
+ waterfallBlogPost: post(id: $id) {
+ id
+ title
+ body
+ authorId
+ createdAt
+ }
+ }
+`
+
+export const Loading = () => Loading...
+
+export const Empty = () => Empty
+
+export const Failure = ({
+ error,
+}: CellFailureProps) => (
+ Error: {error?.message}
+)
+
+export const Success = ({
+ waterfallBlogPost,
+}: CellSuccessProps<
+ FindWaterfallBlogPostQuery,
+ FindWaterfallBlogPostQueryVariables
+>) => (
+
+ {waterfallBlogPost && (
+ <>
+
+
+ {new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date(waterfallBlogPost.createdAt))}{' '}
+ - By:
+
+
+ {waterfallBlogPost.title}
+
+
+
+ {waterfallBlogPost.body}
+
+ >
+ )}
+
+)
diff --git a/__fixtures__/esm-test-project/web/src/entry.client.tsx b/__fixtures__/esm-test-project/web/src/entry.client.tsx
new file mode 100644
index 0000000000..915c14d76d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/entry.client.tsx
@@ -0,0 +1,35 @@
+import { hydrateRoot, createRoot } from 'react-dom/client'
+
+import App from './App'
+import Routes from './Routes'
+
+/**
+ * When `#redwood-app` isn't empty then it's very likely that you're using
+ * prerendering. So React attaches event listeners to the existing markup
+ * rather than replacing it.
+ * https://react.dev/reference/react-dom/client/hydrateRoot
+ */
+const redwoodAppElement = document.getElementById('redwood-app')
+
+if (!redwoodAppElement) {
+ throw new Error(
+ "Could not find an element with ID 'redwood-app'. Please ensure it " +
+ "exists in your 'web/src/index.html' file."
+ )
+}
+
+if (redwoodAppElement.children?.length > 0) {
+ hydrateRoot(
+ redwoodAppElement,
+
+
+
+ )
+} else {
+ const root = createRoot(redwoodAppElement)
+ root.render(
+
+
+
+ )
+}
diff --git a/__fixtures__/esm-test-project/web/src/index.css b/__fixtures__/esm-test-project/web/src/index.css
new file mode 100644
index 0000000000..b31cb3378f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/index.css
@@ -0,0 +1,13 @@
+/**
+ * START --- SETUP TAILWINDCSS EDIT
+ *
+ * `yarn rw setup ui tailwindcss` placed these directives here
+ * to inject Tailwind's styles into your CSS.
+ * For more information, see: https://tailwindcss.com/docs/installation
+ */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+/**
+ * END --- SETUP TAILWINDCSS EDIT
+ */
diff --git a/__fixtures__/esm-test-project/web/src/index.html b/__fixtures__/esm-test-project/web/src/index.html
new file mode 100644
index 0000000000..e240b8eb8c
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__fixtures__/esm-test-project/web/src/layouts/.keep b/__fixtures__/esm-test-project/web/src/layouts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
new file mode 100644
index 0000000000..43f3b5d106
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogLayout from './BlogLayout'
+
+const meta: Meta = {
+ component: BlogLayout,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
new file mode 100644
index 0000000000..60ff1b9368
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogLayout from './BlogLayout'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('BlogLayout', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
new file mode 100644
index 0000000000..87af741fa0
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx
@@ -0,0 +1,85 @@
+type BlogLayoutProps = {
+ children?: React.ReactNode
+}
+
+import { Link, NavLink, routes } from '@cedarjs/router'
+
+import { useAuth } from 'src/auth'
+
+const BlogLayout = ({ children }: BlogLayoutProps) => {
+ const { logOut, isAuthenticated } = useAuth()
+
+ return (
+ <>
+
+
+
+ Cedar Blog
+
+
+
+
+
+
+ About
+
+
+
+
+ Contact Us
+
+
+
+
+ Admin
+
+
+ {isAuthenticated && (
+
+
+ Log Out
+
+
+ )}
+ {!isAuthenticated && (
+
+
+ Log In
+
+
+ )}
+
+
+
+
+ {children}
+
+ >
+ )
+}
+
+export default BlogLayout
diff --git a/__fixtures__/esm-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/__fixtures__/esm-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
new file mode 100644
index 0000000000..626f0d5cc5
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
@@ -0,0 +1,37 @@
+import { Link, routes } from '@cedarjs/router'
+import { Toaster } from '@cedarjs/web/toast'
+
+type LayoutProps = {
+ title: string
+ titleTo: keyof typeof routes
+ buttonLabel: string
+ buttonTo: keyof typeof routes
+ children: React.ReactNode
+}
+
+const ScaffoldLayout = ({
+ title,
+ titleTo,
+ buttonLabel,
+ buttonTo,
+ children,
+}: LayoutProps) => {
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ +
{buttonLabel}
+
+
+
{children}
+
+ )
+}
+
+export default ScaffoldLayout
diff --git a/__fixtures__/esm-test-project/web/src/lib/formatters.test.tsx b/__fixtures__/esm-test-project/web/src/lib/formatters.test.tsx
new file mode 100644
index 0000000000..0383c42716
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/lib/formatters.test.tsx
@@ -0,0 +1,192 @@
+import { render, waitFor, screen } from '@cedarjs/testing/web'
+
+import {
+ formatEnum,
+ jsonTruncate,
+ truncate,
+ timeTag,
+ jsonDisplay,
+ checkboxInputTag,
+} from './formatters.js'
+
+describe('formatEnum', () => {
+ it('handles nullish values', () => {
+ expect(formatEnum(null)).toEqual('')
+ expect(formatEnum('')).toEqual('')
+ expect(formatEnum(undefined)).toEqual('')
+ })
+
+ it('formats a list of values', () => {
+ expect(
+ formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET'])
+ ).toEqual('Red, Orange, Yellow, Green, Blue, Violet')
+ })
+
+ it('formats a single value', () => {
+ expect(formatEnum('DARK_BLUE')).toEqual('Dark blue')
+ })
+
+ it('returns an empty string for values of the wrong type (for JS projects)', () => {
+ // @ts-expect-error - Testing JS scenario
+ expect(formatEnum(5)).toEqual('')
+ })
+})
+
+describe('truncate', () => {
+ it('truncates really long strings', () => {
+ expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000)
+ expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/)
+ })
+
+ it('does not modify short strings', () => {
+ expect(truncate('Short strinG')).toEqual('Short strinG')
+ })
+
+ it('adds ... to the end of truncated strings', () => {
+ expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/)
+ })
+
+ it('accepts numbers', () => {
+ expect(truncate(123)).toEqual('123')
+ expect(truncate(0)).toEqual('0')
+ expect(truncate(0o000)).toEqual('0')
+ })
+
+ it('handles arguments of invalid type', () => {
+ // @ts-expect-error - Testing JS scenario
+ expect(truncate(false)).toEqual('false')
+
+ expect(truncate(undefined)).toEqual('')
+ expect(truncate(null)).toEqual('')
+ })
+})
+
+describe('jsonTruncate', () => {
+ it('truncates large json structures', () => {
+ expect(
+ jsonTruncate({
+ foo: 'foo',
+ bar: 'bar',
+ baz: 'baz',
+ kittens: 'kittens meow',
+ bazinga: 'Sheldon',
+ nested: {
+ foobar: 'I have no imagination',
+ two: 'Second nested item',
+ },
+ five: 5,
+ bool: false,
+ })
+ ).toMatch(/.+\n.+\w\.\.\.$/s)
+ })
+})
+
+describe('timeTag', () => {
+ it('renders a date', async () => {
+ render({timeTag(new Date('1970-08-20').toUTCString())}
)
+
+ await waitFor(() => screen.getByText(/1970.*00:00:00/))
+ })
+
+ it('can take an empty input string', async () => {
+ expect(timeTag('')).toEqual('')
+ })
+})
+
+describe('jsonDisplay', () => {
+ it('produces the correct output', () => {
+ expect(
+ jsonDisplay({
+ title: 'TOML Example (but in JSON)',
+ database: {
+ data: [['delta', 'phi'], [3.14]],
+ enabled: true,
+ ports: [8000, 8001, 8002],
+ temp_targets: {
+ case: 72.0,
+ cpu: 79.5,
+ },
+ },
+ owner: {
+ dob: '1979-05-27T07:32:00-08:00',
+ name: 'Tom Preston-Werner',
+ },
+ servers: {
+ alpha: {
+ ip: '10.0.0.1',
+ role: 'frontend',
+ },
+ beta: {
+ ip: '10.0.0.2',
+ role: 'backend',
+ },
+ },
+ })
+ ).toMatchInlineSnapshot(`
+
+
+ {
+ "title": "TOML Example (but in JSON)",
+ "database": {
+ "data": [
+ [
+ "delta",
+ "phi"
+ ],
+ [
+ 3.14
+ ]
+ ],
+ "enabled": true,
+ "ports": [
+ 8000,
+ 8001,
+ 8002
+ ],
+ "temp_targets": {
+ "case": 72,
+ "cpu": 79.5
+ }
+ },
+ "owner": {
+ "dob": "1979-05-27T07:32:00-08:00",
+ "name": "Tom Preston-Werner"
+ },
+ "servers": {
+ "alpha": {
+ "ip": "10.0.0.1",
+ "role": "frontend"
+ },
+ "beta": {
+ "ip": "10.0.0.2",
+ "role": "backend"
+ }
+ }
+ }
+
+
+ `)
+ })
+})
+
+describe('checkboxInputTag', () => {
+ it('can be checked', () => {
+ render(checkboxInputTag(true))
+ expect(screen.getByRole('checkbox')).toBeChecked()
+ })
+
+ it('can be unchecked', () => {
+ render(checkboxInputTag(false))
+ expect(screen.getByRole('checkbox')).not.toBeChecked()
+ })
+
+ it('is disabled when checked', () => {
+ render(checkboxInputTag(true))
+ expect(screen.getByRole('checkbox')).toBeDisabled()
+ })
+
+ it('is disabled when unchecked', () => {
+ render(checkboxInputTag(false))
+ expect(screen.getByRole('checkbox')).toBeDisabled()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/lib/formatters.tsx b/__fixtures__/esm-test-project/web/src/lib/formatters.tsx
new file mode 100644
index 0000000000..8ab9e806e3
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/lib/formatters.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+
+import humanize from 'humanize-string'
+
+const MAX_STRING_LENGTH = 150
+
+export const formatEnum = (values: string | string[] | null | undefined) => {
+ let output = ''
+
+ if (Array.isArray(values)) {
+ const humanizedValues = values.map((value) => humanize(value))
+ output = humanizedValues.join(', ')
+ } else if (typeof values === 'string') {
+ output = humanize(values)
+ }
+
+ return output
+}
+
+export const jsonDisplay = (obj: unknown) => {
+ return (
+
+ {JSON.stringify(obj, null, 2)}
+
+ )
+}
+
+export const truncate = (value: string | number) => {
+ let output = value?.toString() ?? ''
+
+ if (output.length > MAX_STRING_LENGTH) {
+ output = output.substring(0, MAX_STRING_LENGTH) + '...'
+ }
+
+ return output
+}
+
+export const jsonTruncate = (obj: unknown) => {
+ return truncate(JSON.stringify(obj, null, 2))
+}
+
+export const timeTag = (dateTime?: string) => {
+ let output: string | JSX.Element = ''
+
+ if (dateTime) {
+ output = (
+
+ {new Date(dateTime).toUTCString()}
+
+ )
+ }
+
+ return output
+}
+
+export const checkboxInputTag = (checked: boolean) => {
+ return
+}
diff --git a/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
new file mode 100644
index 0000000000..b8259100eb
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import AboutPage from './AboutPage'
+
+const meta: Meta = {
+ component: AboutPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
new file mode 100644
index 0000000000..13f28830eb
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import AboutPage from './AboutPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('AboutPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.tsx
new file mode 100644
index 0000000000..b0476d6e22
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/AboutPage/AboutPage.tsx
@@ -0,0 +1,10 @@
+const AboutPage = () => {
+ return (
+
+ This site was created to demonstrate my mastery of Redwood: Look on my
+ works, ye mighty, and despair!
+
+ )
+}
+
+export default AboutPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
new file mode 100644
index 0000000000..e29f84e457
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts
@@ -0,0 +1,5 @@
+import { db } from '$api/src/lib/db.js'
+
+export async function routeParameters() {
+ return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id }))
+}
diff --git a/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
new file mode 100644
index 0000000000..b8abecc304
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BlogPostPage from './BlogPostPage'
+
+const meta: Meta = {
+ component: BlogPostPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
new file mode 100644
index 0000000000..1e1e4be5fb
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import BlogPostPage from './BlogPostPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('BlogPostPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
new file mode 100644
index 0000000000..52aac7a3d6
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx
@@ -0,0 +1,20 @@
+// import { Link, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+
+type BlogPostPageProps = {
+ id: number
+}
+
+import BlogPostCell from 'src/components/BlogPostCell'
+
+const BlogPostPage = ({ id }: BlogPostPageProps) => {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+export default BlogPostPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
new file mode 100644
index 0000000000..9af63b0a3d
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx
@@ -0,0 +1,11 @@
+import ContactCell from 'src/components/Contact/ContactCell'
+
+type ContactPageProps = {
+ id: number
+}
+
+const ContactPage = ({ id }: ContactPageProps) => {
+ return
+}
+
+export default ContactPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
new file mode 100644
index 0000000000..7bc4048094
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx
@@ -0,0 +1,7 @@
+import ContactsCell from 'src/components/Contact/ContactsCell'
+
+const ContactsPage = () => {
+ return
+}
+
+export default ContactsPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
new file mode 100644
index 0000000000..7241f71f7f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx
@@ -0,0 +1,11 @@
+import EditContactCell from 'src/components/Contact/EditContactCell'
+
+type ContactPageProps = {
+ id: number
+}
+
+const EditContactPage = ({ id }: ContactPageProps) => {
+ return
+}
+
+export default EditContactPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
new file mode 100644
index 0000000000..2d4cc9274e
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx
@@ -0,0 +1,7 @@
+import NewContact from 'src/components/Contact/NewContact'
+
+const NewContactPage = () => {
+ return
+}
+
+export default NewContactPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
new file mode 100644
index 0000000000..80eb779856
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ContactUsPage from './ContactUsPage'
+
+const meta: Meta = {
+ component: ContactUsPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
new file mode 100644
index 0000000000..0731835b58
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import ContactUsPage from './ContactUsPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('ContactUsPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
new file mode 100644
index 0000000000..de6488e1a7
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx
@@ -0,0 +1,133 @@
+import { useState } from 'react'
+
+import { useForm } from 'react-hook-form'
+
+import {
+ Form,
+ TextField,
+ TextAreaField,
+ Submit,
+ FieldError,
+ Label,
+} from '@cedarjs/forms'
+import { useBlocker } from '@cedarjs/router'
+import { useMutation } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+const CREATE_CONTACT = gql`
+ mutation CreateContactMutation($input: CreateContactInput!) {
+ createContact(input: $input) {
+ id
+ }
+ }
+`
+
+const ContactUsPage = () => {
+ const formMethods = useForm()
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const blocker = useBlocker({
+ when: formMethods.formState.isDirty && !isSubmitting,
+ })
+
+ const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
+ onCompleted: () => {
+ toast.success('Thank you for your submission!')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ })
+
+ const onSubmit = async (data) => {
+ setIsSubmitting(true)
+ try {
+ await create({ variables: { input: data } })
+ formMethods.reset(data)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default ContactUsPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
new file mode 100644
index 0000000000..adb222dbce
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import DoublePage from './DoublePage'
+
+const meta: Meta = {
+ component: DoublePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
new file mode 100644
index 0000000000..b308591403
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import DoublePage from './DoublePage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('DoublePage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.tsx b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.tsx
new file mode 100644
index 0000000000..75aef1171e
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/DoublePage/DoublePage.tsx
@@ -0,0 +1,39 @@
+import { Metadata } from '@cedarjs/web'
+
+import test from './test.png'
+
+const DoublePage = () => {
+ return (
+ <>
+
+
+ DoublePage
+
+ This page exists to make sure we don't regress on{' '}
+
+ #7757
+
+
+ For RW#7757 it needs to be a page that is not wrapped in a Set
+
+ We also use this page to make sure we don't regress on{' '}
+
+ #317
+
+
+
+ >
+ )
+}
+
+export default DoublePage
diff --git a/__fixtures__/esm-test-project/web/src/pages/DoublePage/test.png b/__fixtures__/esm-test-project/web/src/pages/DoublePage/test.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+
+
+ Something went wrong
+
+
+
+ ))
diff --git a/__fixtures__/esm-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/__fixtures__/esm-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
new file mode 100644
index 0000000000..6f7236d1d0
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -0,0 +1,94 @@
+import { useEffect, useRef } from 'react'
+
+import { Form, Label, TextField, Submit, FieldError } from '@cedarjs/forms'
+import { navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const ForgotPasswordPage = () => {
+ const { isAuthenticated, forgotPassword } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef?.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: { username: string }) => {
+ const response = await forgotPassword(data.username)
+
+ if (response.error) {
+ toast.error(response.error)
+ } else {
+ // The function `forgotPassword.handler` in api/src/functions/auth.js has
+ // been invoked, let the user know how to get the link to reset their
+ // password (sent in email, perhaps?)
+ toast.success(
+ 'A link to reset your password was sent to ' + response.email
+ )
+ navigate(routes.login())
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default ForgotPasswordPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.stories.tsx
new file mode 100644
index 0000000000..d9631ae657
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import HomePage from './HomePage'
+
+const meta: Meta = {
+ component: HomePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.test.tsx
new file mode 100644
index 0000000000..a09c9218fb
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import HomePage from './HomePage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('HomePage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.tsx b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.tsx
new file mode 100644
index 0000000000..d86fe65702
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/HomePage/HomePage.tsx
@@ -0,0 +1,7 @@
+import BlogPostsCell from 'src/components/BlogPostsCell'
+
+const HomePage = () => {
+ return
+}
+
+export default HomePage
diff --git a/__fixtures__/esm-test-project/web/src/pages/LoginPage/LoginPage.tsx b/__fixtures__/esm-test-project/web/src/pages/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000000..fced4f6181
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/LoginPage/LoginPage.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useRef } from 'react'
+
+import {
+ Form,
+ Label,
+ TextField,
+ PasswordField,
+ Submit,
+ FieldError,
+} from '@cedarjs/forms'
+import { Link, navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const LoginPage = () => {
+ const { isAuthenticated, logIn } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await logIn({
+ username: data.username,
+ password: data.password,
+ })
+
+ if (response.message) {
+ toast(response.message)
+ } else if (response.error) {
+ toast.error(response.error)
+ } else {
+ toast.success('Welcome back!')
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Don't have an account? {' '}
+
+ Sign up!
+
+
+
+
+ >
+ )
+}
+
+export default LoginPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx b/__fixtures__/esm-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..92ef916989
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,44 @@
+export default () => (
+
+
+
+
+ 404 Page Not Found
+
+
+
+)
diff --git a/__fixtures__/esm-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
new file mode 100644
index 0000000000..f3f8c7bfc8
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Post/EditPostPage/EditPostPage.tsx
@@ -0,0 +1,11 @@
+import EditPostCell from 'src/components/Post/EditPostCell'
+
+type PostPageProps = {
+ id: number
+}
+
+const EditPostPage = ({ id }: PostPageProps) => {
+ return
+}
+
+export default EditPostPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
new file mode 100644
index 0000000000..0b3c453cc3
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Post/NewPostPage/NewPostPage.tsx
@@ -0,0 +1,7 @@
+import NewPost from 'src/components/Post/NewPost'
+
+const NewPostPage = () => {
+ return
+}
+
+export default NewPostPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Post/PostPage/PostPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Post/PostPage/PostPage.tsx
new file mode 100644
index 0000000000..ca4048740a
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Post/PostPage/PostPage.tsx
@@ -0,0 +1,11 @@
+import PostCell from 'src/components/Post/PostCell'
+
+type PostPageProps = {
+ id: number
+}
+
+const PostPage = ({ id }: PostPageProps) => {
+ return
+}
+
+export default PostPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx b/__fixtures__/esm-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
new file mode 100644
index 0000000000..f5b3668d40
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/Post/PostsPage/PostsPage.tsx
@@ -0,0 +1,7 @@
+import PostsCell from 'src/components/Post/PostsCell'
+
+const PostsPage = () => {
+ return
+}
+
+export default PostsPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
new file mode 100644
index 0000000000..ebc171846e
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ProfilePage from './ProfilePage'
+
+const meta: Meta = {
+ component: ProfilePage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
new file mode 100644
index 0000000000..027edfd387
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.test.tsx
@@ -0,0 +1,21 @@
+import { render, waitFor, screen } from '@cedarjs/testing/web'
+
+import ProfilePage from './ProfilePage'
+
+describe('ProfilePage', () => {
+ it('renders successfully', async () => {
+ mockCurrentUser({
+ email: 'danny@bazinga.com',
+ id: 84849020,
+ roles: 'BAZINGA',
+ })
+
+ await waitFor(async () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+
+ expect(await screen.findByText('danny@bazinga.com')).toBeInTheDocument()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.tsx b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
new file mode 100644
index 0000000000..0209c00efe
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ProfilePage/ProfilePage.tsx
@@ -0,0 +1,55 @@
+import { Metadata } from '@cedarjs/web'
+
+import { useAuth } from 'src/auth'
+// import { Link, routes } from '@cedarjs/router'
+
+const ProfilePage = () => {
+ const { currentUser, isAuthenticated, hasRole, loading } = useAuth()
+
+ if (loading) {
+ return Loading...
+ }
+
+ return (
+ <>
+
+
+ Profile
+
+
+
+
+ Key
+ Value
+
+
+
+
+ ID
+ {currentUser.id}
+
+
+ ROLES
+ {currentUser.roles}
+
+
+ EMAIL
+ {currentUser.email}
+
+
+
+ isAuthenticated
+ {JSON.stringify(isAuthenticated)}
+
+
+
+ Is Admin
+ {JSON.stringify(hasRole('ADMIN'))}
+
+
+
+ >
+ )
+}
+
+export default ProfilePage
diff --git a/__fixtures__/esm-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/__fixtures__/esm-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
new file mode 100644
index 0000000000..fd45ae810f
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
@@ -0,0 +1,115 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { Form, Label, PasswordField, Submit, FieldError } from '@cedarjs/forms'
+import { navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
+ const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
+ useAuth()
+ const [enabled, setEnabled] = useState(true)
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ useEffect(() => {
+ const validateToken = async () => {
+ const response = await validateResetToken(resetToken)
+ if (response.error) {
+ setEnabled(false)
+ toast.error(response.error)
+ } else {
+ setEnabled(true)
+ }
+ }
+ validateToken()
+ }, [resetToken, validateResetToken])
+
+ const passwordRef = useRef(null)
+ useEffect(() => {
+ passwordRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await resetPassword({
+ resetToken,
+ password: data.password,
+ })
+
+ if (response.error) {
+ toast.error(response.error)
+ } else {
+ toast.success('Password changed!')
+ await reauthenticate()
+ navigate(routes.login())
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default ResetPasswordPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/SignupPage/SignupPage.tsx b/__fixtures__/esm-test-project/web/src/pages/SignupPage/SignupPage.tsx
new file mode 100644
index 0000000000..8d4998b7e4
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/SignupPage/SignupPage.tsx
@@ -0,0 +1,147 @@
+import { useEffect, useRef } from 'react'
+
+import {
+ Form,
+ Label,
+ TextField,
+ PasswordField,
+ FieldError,
+ Submit,
+} from '@cedarjs/forms'
+import { Link, navigate, routes } from '@cedarjs/router'
+import { Metadata } from '@cedarjs/web'
+import { toast, Toaster } from '@cedarjs/web/toast'
+
+import { useAuth } from 'src/auth'
+
+const SignupPage = () => {
+ const { isAuthenticated, signUp } = useAuth()
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate(routes.home())
+ }
+ }, [isAuthenticated])
+
+ // focus on username box on page load
+ const usernameRef = useRef(null)
+ useEffect(() => {
+ usernameRef.current?.focus()
+ }, [])
+
+ const onSubmit = async (data: Record) => {
+ const response = await signUp({
+ username: data.username,
+ password: data.password,
+ 'full-name': data['full-name'],
+ })
+
+ if (response.message) {
+ toast(response.message)
+ } else if (response.error) {
+ toast.error(response.error)
+ } else {
+ // user is signed in automatically
+ toast.success('Welcome!')
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Already have an account? {' '}
+
+ Log in!
+
+
+
+
+ >
+ )
+}
+
+export default SignupPage
diff --git a/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
new file mode 100644
index 0000000000..88a6dd0b61
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.routeHooks.ts
@@ -0,0 +1,3 @@
+export async function routeParameters() {
+ return [{ id: 2 }]
+}
diff --git a/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
new file mode 100644
index 0000000000..9b15c73474
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import WaterfallPage from './WaterfallPage'
+
+const meta: Meta = {
+ component: WaterfallPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => {
+ return
+ },
+}
diff --git a/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
new file mode 100644
index 0000000000..211c1a684a
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@cedarjs/testing/web'
+
+import WaterfallPage from './WaterfallPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('WaterfallPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render( )
+ }).not.toThrow()
+ })
+})
diff --git a/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
new file mode 100644
index 0000000000..6c4f24a14c
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/pages/WaterfallPage/WaterfallPage.tsx
@@ -0,0 +1,11 @@
+import WaterfallBlogPostCell from 'src/components/WaterfallBlogPostCell'
+
+type WaterfallPageProps = {
+ id: number
+}
+
+const WaterfallPage = ({ id }: WaterfallPageProps) => (
+
+)
+
+export default WaterfallPage
diff --git a/__fixtures__/esm-test-project/web/src/scaffold.css b/__fixtures__/esm-test-project/web/src/scaffold.css
new file mode 100644
index 0000000000..ffa9142b71
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/src/scaffold.css
@@ -0,0 +1,243 @@
+.rw-scaffold {
+ @apply bg-white text-gray-600;
+}
+.rw-scaffold h1,
+.rw-scaffold h2 {
+ @apply m-0;
+}
+.rw-scaffold a {
+ @apply bg-transparent;
+}
+.rw-scaffold ul {
+ @apply m-0 p-0;
+}
+.rw-scaffold input:-ms-input-placeholder {
+ @apply text-gray-500;
+}
+.rw-scaffold input::-ms-input-placeholder {
+ @apply text-gray-500;
+}
+.rw-scaffold input::placeholder {
+ @apply text-gray-500;
+}
+.rw-header {
+ @apply flex justify-between px-8 py-4;
+}
+.rw-main {
+ @apply mx-4 pb-4;
+}
+.rw-segment {
+ @apply w-full overflow-hidden rounded-lg border border-gray-200;
+ scrollbar-color: theme('colors.zinc.400') transparent;
+}
+.rw-segment::-webkit-scrollbar {
+ height: initial;
+}
+.rw-segment::-webkit-scrollbar-track {
+ @apply rounded-b-[10px] rounded-t-none border-0 border-t border-solid border-gray-200 bg-transparent p-[2px];
+}
+.rw-segment::-webkit-scrollbar-thumb {
+ @apply rounded-full border-[3px] border-solid border-transparent bg-zinc-400 bg-clip-content;
+}
+.rw-segment-header {
+ @apply bg-gray-200 px-4 py-3 text-gray-700;
+}
+.rw-segment-main {
+ @apply bg-gray-100 p-4;
+}
+.rw-link {
+ @apply text-blue-400 underline;
+}
+.rw-link:hover {
+ @apply text-blue-500;
+}
+.rw-forgot-link {
+ @apply mt-1 text-right text-xs text-gray-400 underline;
+}
+.rw-forgot-link:hover {
+ @apply text-blue-500;
+}
+.rw-heading {
+ @apply font-semibold;
+}
+.rw-heading.rw-heading-primary {
+ @apply text-xl;
+}
+.rw-heading.rw-heading-secondary {
+ @apply text-sm;
+}
+.rw-heading .rw-link {
+ @apply text-gray-600 no-underline;
+}
+.rw-heading .rw-link:hover {
+ @apply text-gray-900 underline;
+}
+.rw-cell-error {
+ @apply text-sm font-semibold;
+}
+.rw-form-wrapper {
+ @apply -mt-4 text-sm;
+}
+.rw-cell-error,
+.rw-form-error-wrapper {
+ @apply my-4 rounded border border-red-100 bg-red-50 p-4 text-red-600;
+}
+.rw-form-error-title {
+ @apply m-0 font-semibold;
+}
+.rw-form-error-list {
+ @apply mt-2 list-inside list-disc;
+}
+.rw-button {
+ @apply flex cursor-pointer justify-center rounded border-0 bg-gray-200 px-4 py-1 text-xs font-semibold uppercase leading-loose tracking-wide text-gray-500 no-underline transition duration-100;
+}
+.rw-button:hover {
+ @apply bg-gray-500 text-white;
+}
+.rw-button.rw-button-small {
+ @apply rounded-sm px-2 py-1 text-xs;
+}
+.rw-button.rw-button-green {
+ @apply bg-green-500 text-white;
+}
+.rw-button.rw-button-green:hover {
+ @apply bg-green-700;
+}
+.rw-button.rw-button-blue {
+ @apply bg-blue-500 text-white;
+}
+.rw-button.rw-button-blue:hover {
+ @apply bg-blue-700;
+}
+.rw-button.rw-button-red {
+ @apply bg-red-500 text-white;
+}
+.rw-button.rw-button-red:hover {
+ @apply bg-red-700 text-white;
+}
+.rw-button-icon {
+ @apply mr-1 text-xl leading-5;
+}
+.rw-button-group {
+ @apply mx-2 my-3 flex justify-center;
+}
+.rw-button-group .rw-button {
+ @apply mx-1;
+}
+.rw-form-wrapper .rw-button-group {
+ @apply mt-8;
+}
+.rw-label {
+ @apply mt-6 block text-left font-semibold text-gray-600;
+}
+.rw-label.rw-label-error {
+ @apply text-red-600;
+}
+.rw-input {
+ @apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none;
+}
+.rw-check-radio-items {
+ @apply flex justify-items-center;
+}
+.rw-check-radio-item-none {
+ @apply text-gray-600;
+}
+.rw-input[type='checkbox'],
+.rw-input[type='radio'] {
+ @apply ml-0 mr-1 mt-1 inline w-4;
+}
+.rw-input:focus {
+ @apply border-gray-400;
+}
+.rw-input-error {
+ @apply border-red-600 text-red-600;
+}
+.rw-input-error:focus {
+ @apply border-red-600 outline-none;
+ box-shadow: 0 0 5px #c53030;
+}
+.rw-field-error {
+ @apply mt-1 block text-xs font-semibold uppercase text-red-600;
+}
+.rw-table-wrapper-responsive {
+ @apply overflow-x-auto;
+}
+.rw-table-wrapper-responsive .rw-table {
+ min-width: 48rem;
+}
+.rw-table {
+ @apply w-full text-sm;
+}
+.rw-table th,
+.rw-table td {
+ @apply p-3;
+}
+.rw-table td {
+ @apply bg-white text-gray-900;
+}
+.rw-table tr:nth-child(odd) td,
+.rw-table tr:nth-child(odd) th {
+ @apply bg-gray-50;
+}
+.rw-table thead tr {
+ @apply bg-gray-200 text-gray-600;
+}
+.rw-table th {
+ @apply text-left font-semibold;
+}
+.rw-table thead th {
+ @apply text-left;
+}
+.rw-table tbody th {
+ @apply text-right;
+}
+@media (min-width: 768px) {
+ .rw-table tbody th {
+ @apply w-1/5;
+ }
+}
+.rw-table tbody tr {
+ @apply border-t border-gray-200;
+}
+.rw-table input {
+ @apply ml-0;
+}
+.rw-table-actions {
+ @apply flex h-4 items-center justify-end pr-1;
+}
+.rw-table-actions .rw-button {
+ @apply bg-transparent;
+}
+.rw-table-actions .rw-button:hover {
+ @apply bg-gray-500 text-white;
+}
+.rw-table-actions .rw-button-blue {
+ @apply text-blue-500;
+}
+.rw-table-actions .rw-button-blue:hover {
+ @apply bg-blue-500 text-white;
+}
+.rw-table-actions .rw-button-red {
+ @apply text-red-600;
+}
+.rw-table-actions .rw-button-red:hover {
+ @apply bg-red-600 text-white;
+}
+.rw-text-center {
+ @apply text-center;
+}
+.rw-login-container {
+ @apply mx-auto my-16 flex w-96 flex-wrap items-center justify-center;
+}
+.rw-login-container .rw-form-wrapper {
+ @apply w-full text-center;
+}
+.rw-login-link {
+ @apply mt-4 w-full text-center text-sm text-gray-600;
+}
+.rw-webauthn-wrapper {
+ @apply mx-4 mt-6 leading-6;
+}
+.rw-webauthn-wrapper h2 {
+ @apply mb-4 text-xl font-bold;
+}
diff --git a/__fixtures__/esm-test-project/web/tsconfig.json b/__fixtures__/esm-test-project/web/tsconfig.json
new file mode 100644
index 0000000000..edfcaff396
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/web/src",
+ "../api/src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/web/src/*",
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$api/*": ["../api/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/web"]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types",
+ "../node_modules/@testing-library"
+ ],
+ "types": ["jest", "jest-dom"],
+ "jsx": "preserve"
+ },
+ "include": [
+ "src",
+ "config",
+ ".storybook/**/*",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types",
+ "./types"
+ ]
+}
diff --git a/__fixtures__/esm-test-project/web/vite.config.ts b/__fixtures__/esm-test-project/web/vite.config.ts
new file mode 100644
index 0000000000..8db6f5be72
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/vite.config.ts
@@ -0,0 +1,21 @@
+///
+
+import dns from 'node:dns'
+
+import { defineConfig } from 'vite'
+
+import { cedar } from '@cedarjs/vite'
+
+// So that Vite will load on localhost instead of `127.0.0.1`.
+// See: https://vitejs.dev/config/server-options.html#server-host.
+dns.setDefaultResultOrder('verbatim')
+
+export default defineConfig(({ mode }) => ({
+ plugins: [cedar({ mode })],
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./vitest.setup.ts'],
+ // Enables global test APIs like describe, it, expect
+ globals: true,
+ },
+}))
diff --git a/__fixtures__/esm-test-project/web/vitest.setup.ts b/__fixtures__/esm-test-project/web/vitest.setup.ts
new file mode 100644
index 0000000000..3c2a688f47
--- /dev/null
+++ b/__fixtures__/esm-test-project/web/vitest.setup.ts
@@ -0,0 +1,12 @@
+import '@testing-library/jest-dom/vitest'
+
+import { cleanup } from '@testing-library/react'
+import { afterEach } from 'vitest'
+
+afterEach(() => {
+ // If vitest globals are enabled testing-library will clean up after each
+ // test automatically, but we don't enable globals, so we have to manually
+ // clean up here
+ // https://testing-library.com/docs/react-testing-library/api/#cleanup
+ cleanup()
+})
From 3493158843d941ca167c3c35828d5282edfd153b Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 9 Aug 2025 12:35:48 +0200
Subject: [PATCH 2/5] Templates for ESM apps
---
.../esm-templates/js/.editorconfig | 10 +++
.../create-cedar-app/esm-templates/js/.env | 15 +++++
.../esm-templates/js/.env.defaults | 19 ++++++
.../esm-templates/js/.env.example | 4 ++
.../esm-templates/js/.redwood/README.md | 44 +++++++++++++
.../esm-templates/js/.vscode/extensions.json | 14 +++++
.../esm-templates/js/.vscode/launch.json | 56 +++++++++++++++++
.../esm-templates/js/.vscode/settings.json | 11 ++++
.../esm-templates/js/.vscode/tasks.json | 29 +++++++++
.../esm-templates/js/.yarnrc.yml | 15 +++++
.../esm-templates/js/README.md | 17 +++++
.../esm-templates/js/api/db/schema.prisma | 24 ++++++++
.../esm-templates/js/api/jsconfig.json | 41 +++++++++++++
.../esm-templates/js/api/package.json | 10 +++
.../src/directives/requireAuth/requireAuth.js | 22 +++++++
.../requireAuth/requireAuth.test.js | 18 ++++++
.../api/src/directives/skipAuth/skipAuth.js | 16 +++++
.../src/directives/skipAuth/skipAuth.test.js | 10 +++
.../js/api/src/functions/graphql.js | 19 ++++++
.../esm-templates/js/api/src/graphql/.keep | 0
.../esm-templates/js/api/src/lib/auth.js | 32 ++++++++++
.../esm-templates/js/api/src/lib/db.js | 26 ++++++++
.../esm-templates/js/api/src/lib/logger.js | 17 +++++
.../esm-templates/js/api/src/services/.keep | 0
.../esm-templates/js/api/vitest.config.js | 10 +++
.../esm-templates/js/gitignore.template | 24 ++++++++
.../esm-templates/js/graphql.config.cjs | 11 ++++
.../esm-templates/js/package.json | 32 ++++++++++
.../esm-templates/js/prettier.config.cjs | 18 ++++++
.../esm-templates/js/redwood.toml | 21 +++++++
.../esm-templates/js/scripts/.keep | 0
.../esm-templates/js/scripts/jsconfig.json | 54 ++++++++++++++++
.../esm-templates/js/scripts/seed.js | 27 ++++++++
.../esm-templates/js/vitest.config.mjs | 7 +++
.../esm-templates/js/web/jsconfig.json | 53 ++++++++++++++++
.../esm-templates/js/web/package.json | 26 ++++++++
.../esm-templates/js/web/public/README.md | 43 +++++++++++++
.../esm-templates/js/web/public/favicon.png | Bin 0 -> 2500 bytes
.../esm-templates/js/web/public/robots.txt | 2 +
.../esm-templates/js/web/src/App.jsx | 16 +++++
.../esm-templates/js/web/src/Routes.jsx | 20 ++++++
.../esm-templates/js/web/src/components/.keep | 0
.../esm-templates/js/web/src/entry.client.jsx | 35 +++++++++++
.../esm-templates/js/web/src/index.css | 0
.../esm-templates/js/web/src/index.html | 15 +++++
.../esm-templates/js/web/src/layouts/.keep | 0
.../pages/FatalErrorPage/FatalErrorPage.jsx | 58 ++++++++++++++++++
.../src/pages/NotFoundPage/NotFoundPage.jsx | 45 ++++++++++++++
.../esm-templates/js/web/vite.config.js | 21 +++++++
.../esm-templates/js/web/vitest.setup.js | 12 ++++
.../esm-templates/ts/.editorconfig | 10 +++
.../create-cedar-app/esm-templates/ts/.env | 15 +++++
.../esm-templates/ts/.env.defaults | 19 ++++++
.../esm-templates/ts/.env.example | 4 ++
.../esm-templates/ts/.redwood/README.md | 44 +++++++++++++
.../esm-templates/ts/.vscode/extensions.json | 14 +++++
.../esm-templates/ts/.vscode/launch.json | 56 +++++++++++++++++
.../esm-templates/ts/.vscode/settings.json | 11 ++++
.../esm-templates/ts/.vscode/tasks.json | 29 +++++++++
.../esm-templates/ts/.yarnrc.yml | 15 +++++
.../esm-templates/ts/README.md | 17 +++++
.../esm-templates/ts/api/db/schema.prisma | 24 ++++++++
.../esm-templates/ts/api/package.json | 10 +++
.../requireAuth/requireAuth.test.ts | 18 ++++++
.../src/directives/requireAuth/requireAuth.ts | 25 ++++++++
.../src/directives/skipAuth/skipAuth.test.ts | 10 +++
.../api/src/directives/skipAuth/skipAuth.ts | 16 +++++
.../ts/api/src/functions/graphql.ts | 19 ++++++
.../esm-templates/ts/api/src/graphql/.keep | 0
.../esm-templates/ts/api/src/lib/auth.ts | 32 ++++++++++
.../esm-templates/ts/api/src/lib/db.ts | 26 ++++++++
.../esm-templates/ts/api/src/lib/logger.ts | 17 +++++
.../esm-templates/ts/api/src/services/.keep | 0
.../esm-templates/ts/api/tsconfig.json | 26 ++++++++
.../esm-templates/ts/api/vitest.config.ts | 10 +++
.../esm-templates/ts/gitignore.template | 24 ++++++++
.../esm-templates/ts/graphql.config.cjs | 11 ++++
.../esm-templates/ts/package.json | 32 ++++++++++
.../esm-templates/ts/prettier.config.cjs | 18 ++++++
.../esm-templates/ts/redwood.toml | 21 +++++++
.../esm-templates/ts/scripts/.keep | 0
.../esm-templates/ts/scripts/seed.ts | 27 ++++++++
.../esm-templates/ts/scripts/tsconfig.json | 29 +++++++++
.../esm-templates/ts/vitest.config.ts | 7 +++
.../esm-templates/ts/web/package.json | 26 ++++++++
.../esm-templates/ts/web/public/README.md | 43 +++++++++++++
.../esm-templates/ts/web/public/favicon.png | Bin 0 -> 2500 bytes
.../esm-templates/ts/web/public/robots.txt | 2 +
.../esm-templates/ts/web/src/App.tsx | 22 +++++++
.../esm-templates/ts/web/src/Routes.tsx | 20 ++++++
.../esm-templates/ts/web/src/components/.keep | 0
.../esm-templates/ts/web/src/entry.client.tsx | 35 +++++++++++
.../esm-templates/ts/web/src/index.css | 0
.../esm-templates/ts/web/src/index.html | 15 +++++
.../esm-templates/ts/web/src/layouts/.keep | 0
.../pages/FatalErrorPage/FatalErrorPage.tsx | 57 +++++++++++++++++
.../src/pages/NotFoundPage/NotFoundPage.tsx | 44 +++++++++++++
.../esm-templates/ts/web/tsconfig.json | 44 +++++++++++++
.../esm-templates/ts/web/vite.config.ts | 21 +++++++
.../esm-templates/ts/web/vitest.setup.ts | 12 ++++
100 files changed, 1996 insertions(+)
create mode 100644 packages/create-cedar-app/esm-templates/js/.editorconfig
create mode 100644 packages/create-cedar-app/esm-templates/js/.env
create mode 100644 packages/create-cedar-app/esm-templates/js/.env.defaults
create mode 100644 packages/create-cedar-app/esm-templates/js/.env.example
create mode 100644 packages/create-cedar-app/esm-templates/js/.redwood/README.md
create mode 100644 packages/create-cedar-app/esm-templates/js/.vscode/extensions.json
create mode 100644 packages/create-cedar-app/esm-templates/js/.vscode/launch.json
create mode 100644 packages/create-cedar-app/esm-templates/js/.vscode/settings.json
create mode 100644 packages/create-cedar-app/esm-templates/js/.vscode/tasks.json
create mode 100644 packages/create-cedar-app/esm-templates/js/.yarnrc.yml
create mode 100644 packages/create-cedar-app/esm-templates/js/README.md
create mode 100644 packages/create-cedar-app/esm-templates/js/api/db/schema.prisma
create mode 100644 packages/create-cedar-app/esm-templates/js/api/jsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/js/api/package.json
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/graphql/.keep
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/lib/db.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js
create mode 100644 packages/create-cedar-app/esm-templates/js/api/src/services/.keep
create mode 100644 packages/create-cedar-app/esm-templates/js/api/vitest.config.js
create mode 100644 packages/create-cedar-app/esm-templates/js/gitignore.template
create mode 100644 packages/create-cedar-app/esm-templates/js/graphql.config.cjs
create mode 100644 packages/create-cedar-app/esm-templates/js/package.json
create mode 100644 packages/create-cedar-app/esm-templates/js/prettier.config.cjs
create mode 100644 packages/create-cedar-app/esm-templates/js/redwood.toml
create mode 100644 packages/create-cedar-app/esm-templates/js/scripts/.keep
create mode 100644 packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/js/scripts/seed.js
create mode 100644 packages/create-cedar-app/esm-templates/js/vitest.config.mjs
create mode 100644 packages/create-cedar-app/esm-templates/js/web/jsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/js/web/package.json
create mode 100644 packages/create-cedar-app/esm-templates/js/web/public/README.md
create mode 100644 packages/create-cedar-app/esm-templates/js/web/public/favicon.png
create mode 100644 packages/create-cedar-app/esm-templates/js/web/public/robots.txt
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/App.jsx
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/components/.keep
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/index.css
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/index.html
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/layouts/.keep
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
create mode 100644 packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx
create mode 100644 packages/create-cedar-app/esm-templates/js/web/vite.config.js
create mode 100644 packages/create-cedar-app/esm-templates/js/web/vitest.setup.js
create mode 100644 packages/create-cedar-app/esm-templates/ts/.editorconfig
create mode 100644 packages/create-cedar-app/esm-templates/ts/.env
create mode 100644 packages/create-cedar-app/esm-templates/ts/.env.defaults
create mode 100644 packages/create-cedar-app/esm-templates/ts/.env.example
create mode 100644 packages/create-cedar-app/esm-templates/ts/.redwood/README.md
create mode 100644 packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/.vscode/launch.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/.vscode/settings.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/.yarnrc.yml
create mode 100644 packages/create-cedar-app/esm-templates/ts/README.md
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/package.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/graphql/.keep
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/src/services/.keep
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/tsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/gitignore.template
create mode 100644 packages/create-cedar-app/esm-templates/ts/graphql.config.cjs
create mode 100644 packages/create-cedar-app/esm-templates/ts/package.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/prettier.config.cjs
create mode 100644 packages/create-cedar-app/esm-templates/ts/redwood.toml
create mode 100644 packages/create-cedar-app/esm-templates/ts/scripts/.keep
create mode 100644 packages/create-cedar-app/esm-templates/ts/scripts/seed.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/vitest.config.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/package.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/public/README.md
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/public/favicon.png
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/public/robots.txt
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/App.tsx
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/components/.keep
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/index.css
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/index.html
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/layouts/.keep
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/tsconfig.json
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/vite.config.ts
create mode 100644 packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts
diff --git a/packages/create-cedar-app/esm-templates/js/.editorconfig b/packages/create-cedar-app/esm-templates/js/.editorconfig
new file mode 100644
index 0000000000..ae10a5cce3
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/packages/create-cedar-app/esm-templates/js/.env b/packages/create-cedar-app/esm-templates/js/.env
new file mode 100644
index 0000000000..037d6a247f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.env
@@ -0,0 +1,15 @@
+# THIS FILE SHOULD NOT BE CHECKED INTO YOUR VERSION CONTROL SYSTEM
+#
+# Environment variables set here will override those in .env.defaults.
+# Any environment variables you need in production you will need to setup with
+# your hosting provider. For example in Netlify you can add environment
+# variables in Settings > Build & Deploy > environment
+#
+# DATABASE_URL=postgres://user:pass@postgreshost.com:5432/database_name
+# TEST_DATABASE_URL=postgres://user:pass@postgreshost.com:5432/test_database_name
+#
+# Sets an app-specific secret used to sign and verify your own app's webhooks.
+# For example if you schedule a cron job with a signed payload that later will
+# then invoke your api-side webhook function you will use this secret to sign and the verify.
+# Important: Please change this default to a strong password or other secret
+# WEBHOOK_SECRET=THIS_IS_NOT_SECRET_PLEASE_CHANGE
diff --git a/packages/create-cedar-app/esm-templates/js/.env.defaults b/packages/create-cedar-app/esm-templates/js/.env.defaults
new file mode 100644
index 0000000000..fb88fb33b3
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.env.defaults
@@ -0,0 +1,19 @@
+# These environment variables will be used by default if you do not create any
+# yourself in .env. This file should be safe to check into your version control
+# system. Any custom values should go in .env and .env should *not* be checked
+# into version control.
+
+# schema.prisma defaults
+DATABASE_URL=file:./dev.db
+
+# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
+# TEST_DATABASE_URL=file:./.redwood/test.db
+
+# disables Prisma CLI update notifier
+PRISMA_HIDE_UPDATE_MESSAGE=true
+
+# Option to override the current environment's default api-side log level
+# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise.
+# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
+# Ordered by how verbose they are: trace | debug | info | warn | error | silent
+# LOG_LEVEL=debug
diff --git a/packages/create-cedar-app/esm-templates/js/.env.example b/packages/create-cedar-app/esm-templates/js/.env.example
new file mode 100644
index 0000000000..2a2de6c026
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.env.example
@@ -0,0 +1,4 @@
+# DATABASE_URL=file:./dev.db
+# TEST_DATABASE_URL=file:./.redwood/test.db
+# PRISMA_HIDE_UPDATE_MESSAGE=true
+# LOG_LEVEL=trace
diff --git a/packages/create-cedar-app/esm-templates/js/.redwood/README.md b/packages/create-cedar-app/esm-templates/js/.redwood/README.md
new file mode 100644
index 0000000000..8a1bf5738b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.redwood/README.md
@@ -0,0 +1,44 @@
+# .redwood
+
+## What is this directory?
+
+Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project.
+
+## Do I need to do anything with this directory?
+
+No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood.
+
+You don't need to commit any other contents of this directory to your version control system. It's ignored by default.
+
+## What's in this directory?
+
+### Files
+
+| Name | Description |
+| :---------------- | :----------------------------------------------------------------------------------------------------------------- |
+| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. |
+| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. |
+| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. |
+| test.db | The sqlite database used when running tests. |
+
+### Directories
+
+| Name | Description |
+| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
+| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. |
+| logs | Stores log files for background tasks such as update checking. |
+| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. |
+| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. |
+| types | Stores the results of type generation. |
+| updateCheck | Stores a file which contains the results of checking for Redwood updates. |
+| studio | Used to store data for `rw studio` |
+
+We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list.
+
+### Telemetry
+
+RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com).
+
+### Have any questions?
+
+Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions.
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/extensions.json b/packages/create-cedar-app/esm-templates/js/.vscode/extensions.json
new file mode 100644
index 0000000000..7fc50a119f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ofhumanbondage.react-proptypes-intellisense",
+ "mgmcdermott.vscode-language-babel",
+ "wix.vscode-import-cost",
+ "pflannery.vscode-versionlens",
+ "editorconfig.editorconfig",
+ "prisma.prisma",
+ "graphql.vscode-graphql"
+ ],
+ "unwantedRecommendations": []
+}
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/launch.json b/packages/create-cedar-app/esm-templates/js/.vscode/launch.json
new file mode 100644
index 0000000000..37257c5c15
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.vscode/launch.json
@@ -0,0 +1,56 @@
+{
+ "version": "0.3.0",
+ "configurations": [
+ {
+ "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening
+ "name": "Run Dev Server",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "name": "Attach API debugger",
+ "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration
+ "request": "attach",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node",
+ "localRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "remoteRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "sourceMaps": true,
+ "restart": true,
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "name": "Launch Web debugger",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:8910",
+ "webRoot": "${workspaceRoot}/web/src",
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "command": "yarn redwood test api",
+ "name": "Test api",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "command": "yarn redwood test web",
+ "name": "Test web",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ ],
+ "compounds": [
+ {
+ "name": "Start Debug",
+ "configurations": [
+ "Run Dev Server",
+ "Attach API debugger",
+ "Launch Web debugger"
+ ],
+ "stopAll": true
+ }
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/settings.json b/packages/create-cedar-app/esm-templates/js/.vscode/settings.json
new file mode 100644
index 0000000000..6887d360eb
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "editor.tabSize": 2,
+ "files.trimTrailingWhitespace": true,
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ },
+ "[prisma]": {
+ "editor.formatOnSave": true
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/tasks.json b/packages/create-cedar-app/esm-templates/js/.vscode/tasks.json
new file mode 100644
index 0000000000..549249ec63
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "WaitForDevServer",
+ "group": "none",
+ "type": "shell",
+ "command": "bash",
+ "args": [
+ "-c",
+ "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;"
+ ],
+ "windows": {
+ "command": "powershell",
+ "args": [
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };"
+ ]
+ },
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "panel": "shared",
+ "close": true
+ }
+ },
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/js/.yarnrc.yml b/packages/create-cedar-app/esm-templates/js/.yarnrc.yml
new file mode 100644
index 0000000000..e8c5d50aa7
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/.yarnrc.yml
@@ -0,0 +1,15 @@
+# Yarn's manifest file. You can configure yarn here.
+# See https://yarnpkg.com/configuration/yarnrc.
+
+# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option.
+compressionLevel: 0
+
+enableGlobalCache: true
+
+# Lets yarn use hardlinks inside `node_modules` to dedupe packages.
+# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store.
+nmMode: hardlinks-local
+
+# How to install Node packages.
+# Heads up: right now, Redwood expects this to be `node-modules`.
+nodeLinker: node-modules
diff --git a/packages/create-cedar-app/esm-templates/js/README.md b/packages/create-cedar-app/esm-templates/js/README.md
new file mode 100644
index 0000000000..4487e7f63d
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/README.md
@@ -0,0 +1,17 @@
+# README
+
+Welcome to your new [CedarJS](https://cedarjs.com) project!
+
+Start by installing dependencies:
+
+```
+yarn install
+```
+
+Then start the development server:
+
+```
+yarn redwood dev
+```
+
+Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources.
diff --git a/packages/create-cedar-app/esm-templates/js/api/db/schema.prisma b/packages/create-cedar-app/esm-templates/js/api/db/schema.prisma
new file mode 100644
index 0000000000..8c8666606f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/db/schema.prisma
@@ -0,0 +1,24 @@
+// Don't forget to tell Prisma about your edits to this file using
+// `yarn rw prisma migrate dev` or `yarn rw prisma db push`.
+// `migrate` is like committing while `push` is for prototyping.
+// Read more about both here:
+// https://www.prisma.io/docs/orm/prisma-migrate
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+// Define your own datamodels here and run `yarn redwood prisma migrate dev`
+// to create migrations for them and apply to your dev DB.
+// TODO: Please remove the following example:
+model UserExample {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
diff --git a/packages/create-cedar-app/esm-templates/js/api/jsconfig.json b/packages/create-cedar-app/esm-templates/js/api/jsconfig.json
new file mode 100644
index 0000000000..3c17df519a
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/jsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "types/*": [
+ "./types/*",
+ "../types/*"
+ ],
+ "@cedarjs/testing": [
+ "../node_modules/@cedarjs/testing/api"
+ ]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types"
+ ],
+ "types": [
+ "jest"
+ ],
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/api-*",
+ "../types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/js/api/package.json b/packages/create-cedar-app/esm-templates/js/api/package.json
new file mode 100644
index 0000000000..f5f58e869c
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "api",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@cedarjs/api": "0.0.5",
+ "@cedarjs/graphql-server": "0.0.5"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js b/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js
new file mode 100644
index 0000000000..7e00172db5
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js
@@ -0,0 +1,22 @@
+import { gql } from 'graphql-tag'
+
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+import { requireAuth as applicationRequireAuth } from 'src/lib/auth.js'
+
+export const schema = gql`
+ """
+ Use to check whether or not a user is authenticated and is associated
+ with an optional set of roles.
+ """
+ directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+`
+
+const validate = ({ directiveArgs }) => {
+ const { roles } = directiveArgs
+ applicationRequireAuth({ roles })
+}
+
+const requireAuth = createValidatorDirective(schema, validate)
+
+export default requireAuth
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js b/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js
new file mode 100644
index 0000000000..1de4ac9b65
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js
@@ -0,0 +1,18 @@
+import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
+
+import requireAuth from './requireAuth.js'
+
+describe('requireAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(requireAuth.schema).toBeTruthy()
+ expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
+ })
+
+ it('requireAuth has stub implementation. Should not throw when current user', () => {
+ // If you want to set values in context, pass it through e.g.
+ // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
+ const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
+
+ expect(mockExecution).not.toThrowError()
+ })
+})
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js b/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js
new file mode 100644
index 0000000000..fdea5cf17b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js
@@ -0,0 +1,16 @@
+import { gql } from 'graphql-tag'
+
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+export const schema = gql`
+ """
+ Use to skip authentication checks and allow public access.
+ """
+ directive @skipAuth on FIELD_DEFINITION
+`
+
+const skipAuth = createValidatorDirective(schema, () => {
+ return
+})
+
+export default skipAuth
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js b/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js
new file mode 100644
index 0000000000..68c006bdae
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js
@@ -0,0 +1,10 @@
+import { getDirectiveName } from '@cedarjs/testing/api'
+
+import skipAuth from './skipAuth.js'
+
+describe('skipAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(skipAuth.schema).toBeTruthy()
+ expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
+ })
+})
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js b/packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js
new file mode 100644
index 0000000000..1cbf10059a
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js
@@ -0,0 +1,19 @@
+import { createGraphQLHandler } from '@cedarjs/graphql-server'
+
+import directives from 'src/directives/**/*.{js,ts}'
+import sdls from 'src/graphql/**/*.sdl.{js,ts}'
+import services from 'src/services/**/*.{js,ts}'
+
+import { db } from 'src/lib/db.js'
+import { logger } from 'src/lib/logger.js'
+
+export const handler = createGraphQLHandler({
+ loggerConfig: { logger, options: {} },
+ directives,
+ sdls,
+ services,
+ onException: () => {
+ // Disconnect from your database with an unhandled exception.
+ db.$disconnect()
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/graphql/.keep b/packages/create-cedar-app/esm-templates/js/api/src/graphql/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js b/packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js
new file mode 100644
index 0000000000..c55e5f7535
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js
@@ -0,0 +1,32 @@
+/**
+ * Once you are ready to add authentication to your application
+ * you'll build out requireAuth() with real functionality. For
+ * now we just return `true` so that the calls in services
+ * have something to check against, simulating a logged
+ * in user that is allowed to access that service.
+ *
+ * See https://redwoodjs.com/docs/authentication for more info.
+ */
+export const isAuthenticated = () => {
+ return true
+}
+
+export const hasRole = ({ roles }) => {
+ return roles !== undefined
+}
+
+// This is used by the redwood directive
+// in ./api/src/directives/requireAuth
+
+// Roles are passed in by the requireAuth directive if you have auth setup
+// eslint-disable-next-line no-unused-vars
+export const requireAuth = ({ roles }) => {
+ return isAuthenticated()
+}
+
+export const getCurrentUser = async () => {
+ throw new Error(
+ 'Auth is not set up yet. See https://redwoodjs.com/docs/authentication ' +
+ 'to get started'
+ )
+}
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/db.js b/packages/create-cedar-app/esm-templates/js/api/src/lib/db.js
new file mode 100644
index 0000000000..b6fa93228f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/lib/db.js
@@ -0,0 +1,26 @@
+// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor
+// for options.
+
+import { PrismaClient } from '@prisma/client'
+
+import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'
+
+import { logger } from './logger.js'
+
+const prismaClient = new PrismaClient({
+ log: emitLogLevels(['info', 'warn', 'error']),
+})
+
+handlePrismaLogging({
+ db: prismaClient,
+ logger,
+ logLevels: ['info', 'warn', 'error'],
+})
+
+/**
+ * Global Prisma client extensions should be added here, as $extend
+ * returns a new instance.
+ * export const db = prismaClient.$extend(...)
+ * Add any .$on hooks before using $extend
+ */
+export const db = prismaClient
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js b/packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js
new file mode 100644
index 0000000000..b5f38c1921
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js
@@ -0,0 +1,17 @@
+import { createLogger } from '@cedarjs/api/logger'
+
+/**
+ * Creates a logger with RedwoodLoggerOptions
+ *
+ * These extend and override default LoggerOptions,
+ * can define a destination like a file or other supported pino log transport stream,
+ * and sets whether or not to show the logger configuration settings (defaults to false)
+ *
+ * @param RedwoodLoggerOptions
+ *
+ * RedwoodLoggerOptions have
+ * @param {options} LoggerOptions - defines how to log, such as redaction and format
+ * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file
+ * @param {boolean} showConfig - whether to display logger configuration on initialization
+ */
+export const logger = createLogger({})
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/services/.keep b/packages/create-cedar-app/esm-templates/js/api/src/services/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/api/vitest.config.js b/packages/create-cedar-app/esm-templates/js/api/vitest.config.js
new file mode 100644
index 0000000000..f3de348f75
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/api/vitest.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config'
+
+import { cedarVitestPreset } from '@cedarjs/vite/api'
+
+export default defineConfig({
+ plugins: [cedarVitestPreset()],
+ test: {
+ globals: true,
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/js/gitignore.template b/packages/create-cedar-app/esm-templates/js/gitignore.template
new file mode 100644
index 0000000000..31d9637ede
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/gitignore.template
@@ -0,0 +1,24 @@
+.idea
+.DS_Store
+.env*
+!.env.example
+!.env.defaults
+.netlify
+.redwood/*
+!.redwood/README.md
+dev.db*
+dist
+dist-babel
+node_modules
+yarn-error.log
+web/public/mockServiceWorker.js
+web/types/graphql.d.ts
+api/types/graphql.d.ts
+api/src/lib/generateGraphiQLHeader.*
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/packages/create-cedar-app/esm-templates/js/graphql.config.cjs b/packages/create-cedar-app/esm-templates/js/graphql.config.cjs
new file mode 100644
index 0000000000..d82bff34e0
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/graphql.config.cjs
@@ -0,0 +1,11 @@
+// This file is used by the VSCode GraphQL extension
+
+const { getPaths } = require('@cedarjs/project-config')
+
+/** @type {import('graphql-config').IGraphQLConfig} */
+const config = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
+}
+
+module.exports = config
diff --git a/packages/create-cedar-app/esm-templates/js/package.json b/packages/create-cedar-app/esm-templates/js/package.json
new file mode 100644
index 0000000000..e403c327ae
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/package.json
@@ -0,0 +1,32 @@
+{
+ "private": true,
+ "type": "module",
+ "workspaces": {
+ "packages": [
+ "api",
+ "web"
+ ]
+ },
+ "devDependencies": {
+ "@cedarjs/core": "0.0.5",
+ "@cedarjs/project-config": "0.0.5",
+ "@cedarjs/testing": "0.0.5",
+ "vitest": "3.2.4"
+ },
+ "eslintConfig": {
+ "extends": "@cedarjs/eslint-config",
+ "root": true
+ },
+ "engines": {
+ "node": "=20.x"
+ },
+ "prisma": {
+ "seed": "yarn rw exec seed"
+ },
+ "packageManager": "yarn@4.9.2",
+ "resolutions": {
+ "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
+ "react-is": "19.0.0-rc-f2df5694-20240916",
+ "vite": "5.4.16"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/js/prettier.config.cjs b/packages/create-cedar-app/esm-templates/js/prettier.config.cjs
new file mode 100644
index 0000000000..45058f7aa2
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/prettier.config.cjs
@@ -0,0 +1,18 @@
+// https://prettier.io/docs/en/options.html
+/** @type {import('prettier').RequiredOptions} */
+module.exports = {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ overrides: [
+ {
+ files: 'Routes.*',
+ options: {
+ printWidth: 999,
+ },
+ },
+ ],
+}
diff --git a/packages/create-cedar-app/esm-templates/js/redwood.toml b/packages/create-cedar-app/esm-templates/js/redwood.toml
new file mode 100644
index 0000000000..c2c2be24d6
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/redwood.toml
@@ -0,0 +1,21 @@
+# This file contains the configuration settings for your Redwood app.
+# This file is also what makes your Redwood app a Redwood app.
+# If you remove it and try to run `yarn rw dev`, you'll get an error.
+#
+# For the full list of options, see the "App Configuration: redwood.toml" doc:
+# https://redwoodjs.com/docs/app-configuration-redwood-toml
+
+[web]
+ title = "Cedar App"
+ port = 8910
+ apiUrl = "/.redwood/functions"
+ includeEnvironmentVariables = [
+ # Add any ENV vars that should be available to the web side to this array
+ # See https://redwoodjs.com/docs/environment-variables#web
+ ]
+[api]
+ port = 8911
+[browser]
+ open = true
+[notifications]
+ versionUpdates = ["latest"]
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/.keep b/packages/create-cedar-app/esm-templates/js/scripts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json b/packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json
new file mode 100644
index 0000000000..51ebe307ad
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json
@@ -0,0 +1,54 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "paths": {
+ "$api/*": [
+ "../api/*"
+ ],
+ "api/*": [
+ "../api/*"
+ ],
+ "$api/src/*": [
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "api/src/*": [
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$web/*": [
+ "../web/*"
+ ],
+ "web/*": [
+ "../web/*"
+ ],
+ "$web/src/*": [
+ "../web/src/*",
+ "../.redwood/types/mirror/web/src/*"
+ ],
+ "web/src/*": [
+ "../web/src/*",
+ "../.redwood/types/mirror/web/src/*"
+ ],
+ "types/*": [
+ "../types/*",
+ "../web/types/*",
+ "../api/types/*"
+ ]
+ },
+ "typeRoots": [
+ "../node_modules/@types"
+ ],
+ "jsx": "preserve"
+ },
+ "include": [
+ ".",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/seed.js b/packages/create-cedar-app/esm-templates/js/scripts/seed.js
new file mode 100644
index 0000000000..e070a052a3
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/scripts/seed.js
@@ -0,0 +1,27 @@
+// import { db } from 'api/src/lib/db.js'
+
+// Manually apply seeds via the `yarn rw prisma db seed` command.
+//
+// Seeds automatically run the first time you run the `yarn rw prisma migrate dev`
+// command and every time you run the `yarn rw prisma migrate reset` command.
+//
+// See https://redwoodjs.com/docs/database-seeds for more info
+
+export default async () => {
+ try {
+ // Create your database records here! For example, seed some users:
+ //
+ // const users = [
+ // { name: 'Alice', email: 'alice@cedarjs.com' },
+ // { name: 'Bob', email: 'bob@cedarjs.com' },
+ // ]
+ //
+ // await db.user.createMany({ data: users })
+
+ console.info(
+ '\n No seed data, skipping. See scripts/seed.js to start seeding your database!\n'
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/js/vitest.config.mjs b/packages/create-cedar-app/esm-templates/js/vitest.config.mjs
new file mode 100644
index 0000000000..6c64e7920b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/vitest.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: ['/{*,!(node_modules)/**/}/vite?(st).config.{js,ts}'],
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/js/web/jsconfig.json b/packages/create-cedar-app/esm-templates/js/web/jsconfig.json
new file mode 100644
index 0000000000..ebc6092308
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/jsconfig.json
@@ -0,0 +1,53 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "esModuleInterop": true,
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/web/src",
+ "../api/src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/web/src/*",
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$api/*": [
+ "../api/*"
+ ],
+ "types/*": [
+ "./types/*",
+ "../types/*"
+ ],
+ "@cedarjs/testing": [
+ "../node_modules/@cedarjs/testing/web"
+ ]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types",
+ "../node_modules/@testing-library"
+ ],
+ "types": [
+ "jest",
+ "jest-dom"
+ ],
+ "jsx": "preserve"
+ },
+ "include": [
+ "src",
+ "config",
+ ".storybook/**/*",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types",
+ "./types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/js/web/package.json b/packages/create-cedar-app/esm-templates/js/web/package.json
new file mode 100644
index 0000000000..23c0584006
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "web",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "browserslist": {
+ "development": [
+ "last 1 version"
+ ],
+ "production": [
+ "defaults"
+ ]
+ },
+ "dependencies": {
+ "@cedarjs/forms": "0.0.5",
+ "@cedarjs/router": "0.0.5",
+ "@cedarjs/web": "0.0.5",
+ "react": "19.0.0-rc-f2df5694-20240916",
+ "react-dom": "19.0.0-rc-f2df5694-20240916"
+ },
+ "devDependencies": {
+ "@cedarjs/vite": "0.0.5",
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/js/web/public/README.md b/packages/create-cedar-app/esm-templates/js/web/public/README.md
new file mode 100644
index 0000000000..1b09bf8361
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/public/README.md
@@ -0,0 +1,43 @@
+# Static Assets
+
+Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`.
+
+> Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes.
+
+### Example Use
+
+A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
+
+```
+
+```
+
+and
+
+```
+ alt="Logo" />
+```
+
+## Best Practices
+
+Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc.
+
+In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash.
+
+### Example Asset Import with Vite
+
+Instead of handling our logo image as a static file per the example above, we can do the following:
+
+```
+import React from "react"
+import logo from "./my-logo.jpg"
+
+
+function Header() {
+ return
+}
+
+export default Header
+```
+
+See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html)
diff --git a/packages/create-cedar-app/esm-templates/js/web/public/favicon.png b/packages/create-cedar-app/esm-templates/js/web/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+ {children}
+
+
+)
+
+export default App
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx b/packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx
new file mode 100644
index 0000000000..ae5daa0c22
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx
@@ -0,0 +1,20 @@
+// In this file, all Page components from 'src/pages` are auto-imported. Nested
+// directories are supported, and should be uppercase. Each subdirectory will be
+// prepended onto the component name.
+//
+// Examples:
+//
+// 'src/pages/HomePage/HomePage.js' -> HomePage
+// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
+
+import { Router, Route } from '@cedarjs/router'
+
+const Routes = () => {
+ return (
+
+
+
+ )
+}
+
+export default Routes
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/components/.keep b/packages/create-cedar-app/esm-templates/js/web/src/components/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx b/packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx
new file mode 100644
index 0000000000..915c14d76d
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx
@@ -0,0 +1,35 @@
+import { hydrateRoot, createRoot } from 'react-dom/client'
+
+import App from './App'
+import Routes from './Routes'
+
+/**
+ * When `#redwood-app` isn't empty then it's very likely that you're using
+ * prerendering. So React attaches event listeners to the existing markup
+ * rather than replacing it.
+ * https://react.dev/reference/react-dom/client/hydrateRoot
+ */
+const redwoodAppElement = document.getElementById('redwood-app')
+
+if (!redwoodAppElement) {
+ throw new Error(
+ "Could not find an element with ID 'redwood-app'. Please ensure it " +
+ "exists in your 'web/src/index.html' file."
+ )
+}
+
+if (redwoodAppElement.children?.length > 0) {
+ hydrateRoot(
+ redwoodAppElement,
+
+
+
+ )
+} else {
+ const root = createRoot(redwoodAppElement)
+ root.render(
+
+
+
+ )
+}
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/index.css b/packages/create-cedar-app/esm-templates/js/web/src/index.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/index.html b/packages/create-cedar-app/esm-templates/js/web/src/index.html
new file mode 100644
index 0000000000..e240b8eb8c
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/layouts/.keep b/packages/create-cedar-app/esm-templates/js/web/src/layouts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx b/packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
new file mode 100644
index 0000000000..e89f990317
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
@@ -0,0 +1,58 @@
+// This page will be rendered when an error makes it all the way to the top of the
+// application without being handled by a Javascript catch statement or React error
+// boundary.
+//
+// You can modify this page as you wish, but it is important to keep things simple to
+// avoid the possibility that it will cause its own error. If it does, Redwood will
+// still render a generic error page, but your users will prefer something a bit more
+// thoughtful :)
+
+// This import will be automatically removed when building for production
+import { DevFatalErrorPage } from '@cedarjs/web/dist/components/DevFatalErrorPage'
+
+export default DevFatalErrorPage ||
+ (() => (
+
+
+
+
+
+ Something went wrong
+
+
+
+ ))
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx b/packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx
new file mode 100644
index 0000000000..6628dbeea9
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx
@@ -0,0 +1,45 @@
+export default () => (
+
+
+
+
+
+ 404 Page Not Found
+
+
+
+)
diff --git a/packages/create-cedar-app/esm-templates/js/web/vite.config.js b/packages/create-cedar-app/esm-templates/js/web/vite.config.js
new file mode 100644
index 0000000000..8a7aeb6513
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/vite.config.js
@@ -0,0 +1,21 @@
+///
+
+import dns from 'node:dns'
+
+import { defineConfig } from 'vite'
+
+import { cedar } from '@cedarjs/vite'
+
+// So that Vite will load on localhost instead of `127.0.0.1`.
+// See: https://vitejs.dev/config/server-options.html#server-host.
+dns.setDefaultResultOrder('verbatim')
+
+export default defineConfig(({ mode }) => ({
+ plugins: [cedar({ mode })],
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./vitest.setup.js'],
+ // Enables global test APIs like describe, it, expect
+ globals: true,
+ },
+}))
diff --git a/packages/create-cedar-app/esm-templates/js/web/vitest.setup.js b/packages/create-cedar-app/esm-templates/js/web/vitest.setup.js
new file mode 100644
index 0000000000..3c2a688f47
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/js/web/vitest.setup.js
@@ -0,0 +1,12 @@
+import '@testing-library/jest-dom/vitest'
+
+import { cleanup } from '@testing-library/react'
+import { afterEach } from 'vitest'
+
+afterEach(() => {
+ // If vitest globals are enabled testing-library will clean up after each
+ // test automatically, but we don't enable globals, so we have to manually
+ // clean up here
+ // https://testing-library.com/docs/react-testing-library/api/#cleanup
+ cleanup()
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/.editorconfig b/packages/create-cedar-app/esm-templates/ts/.editorconfig
new file mode 100644
index 0000000000..ae10a5cce3
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/packages/create-cedar-app/esm-templates/ts/.env b/packages/create-cedar-app/esm-templates/ts/.env
new file mode 100644
index 0000000000..037d6a247f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.env
@@ -0,0 +1,15 @@
+# THIS FILE SHOULD NOT BE CHECKED INTO YOUR VERSION CONTROL SYSTEM
+#
+# Environment variables set here will override those in .env.defaults.
+# Any environment variables you need in production you will need to setup with
+# your hosting provider. For example in Netlify you can add environment
+# variables in Settings > Build & Deploy > environment
+#
+# DATABASE_URL=postgres://user:pass@postgreshost.com:5432/database_name
+# TEST_DATABASE_URL=postgres://user:pass@postgreshost.com:5432/test_database_name
+#
+# Sets an app-specific secret used to sign and verify your own app's webhooks.
+# For example if you schedule a cron job with a signed payload that later will
+# then invoke your api-side webhook function you will use this secret to sign and the verify.
+# Important: Please change this default to a strong password or other secret
+# WEBHOOK_SECRET=THIS_IS_NOT_SECRET_PLEASE_CHANGE
diff --git a/packages/create-cedar-app/esm-templates/ts/.env.defaults b/packages/create-cedar-app/esm-templates/ts/.env.defaults
new file mode 100644
index 0000000000..fb88fb33b3
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.env.defaults
@@ -0,0 +1,19 @@
+# These environment variables will be used by default if you do not create any
+# yourself in .env. This file should be safe to check into your version control
+# system. Any custom values should go in .env and .env should *not* be checked
+# into version control.
+
+# schema.prisma defaults
+DATABASE_URL=file:./dev.db
+
+# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set)
+# TEST_DATABASE_URL=file:./.redwood/test.db
+
+# disables Prisma CLI update notifier
+PRISMA_HIDE_UPDATE_MESSAGE=true
+
+# Option to override the current environment's default api-side log level
+# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise.
+# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
+# Ordered by how verbose they are: trace | debug | info | warn | error | silent
+# LOG_LEVEL=debug
diff --git a/packages/create-cedar-app/esm-templates/ts/.env.example b/packages/create-cedar-app/esm-templates/ts/.env.example
new file mode 100644
index 0000000000..2a2de6c026
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.env.example
@@ -0,0 +1,4 @@
+# DATABASE_URL=file:./dev.db
+# TEST_DATABASE_URL=file:./.redwood/test.db
+# PRISMA_HIDE_UPDATE_MESSAGE=true
+# LOG_LEVEL=trace
diff --git a/packages/create-cedar-app/esm-templates/ts/.redwood/README.md b/packages/create-cedar-app/esm-templates/ts/.redwood/README.md
new file mode 100644
index 0000000000..8a1bf5738b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.redwood/README.md
@@ -0,0 +1,44 @@
+# .redwood
+
+## What is this directory?
+
+Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project.
+
+## Do I need to do anything with this directory?
+
+No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood.
+
+You don't need to commit any other contents of this directory to your version control system. It's ignored by default.
+
+## What's in this directory?
+
+### Files
+
+| Name | Description |
+| :---------------- | :----------------------------------------------------------------------------------------------------------------- |
+| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. |
+| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. |
+| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. |
+| test.db | The sqlite database used when running tests. |
+
+### Directories
+
+| Name | Description |
+| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
+| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. |
+| logs | Stores log files for background tasks such as update checking. |
+| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. |
+| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. |
+| types | Stores the results of type generation. |
+| updateCheck | Stores a file which contains the results of checking for Redwood updates. |
+| studio | Used to store data for `rw studio` |
+
+We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list.
+
+### Telemetry
+
+RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com).
+
+### Have any questions?
+
+Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions.
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json b/packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json
new file mode 100644
index 0000000000..7fc50a119f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "eamodio.gitlens",
+ "ofhumanbondage.react-proptypes-intellisense",
+ "mgmcdermott.vscode-language-babel",
+ "wix.vscode-import-cost",
+ "pflannery.vscode-versionlens",
+ "editorconfig.editorconfig",
+ "prisma.prisma",
+ "graphql.vscode-graphql"
+ ],
+ "unwantedRecommendations": []
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/launch.json b/packages/create-cedar-app/esm-templates/ts/.vscode/launch.json
new file mode 100644
index 0000000000..37257c5c15
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.vscode/launch.json
@@ -0,0 +1,56 @@
+{
+ "version": "0.3.0",
+ "configurations": [
+ {
+ "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening
+ "name": "Run Dev Server",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "name": "Attach API debugger",
+ "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration
+ "request": "attach",
+ "skipFiles": [
+ "/**"
+ ],
+ "type": "node",
+ "localRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "remoteRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist",
+ "sourceMaps": true,
+ "restart": true,
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "name": "Launch Web debugger",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:8910",
+ "webRoot": "${workspaceRoot}/web/src",
+ "preLaunchTask": "WaitForDevServer",
+ },
+ {
+ "command": "yarn redwood test api",
+ "name": "Test api",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ {
+ "command": "yarn redwood test web",
+ "name": "Test web",
+ "request": "launch",
+ "type": "node-terminal"
+ },
+ ],
+ "compounds": [
+ {
+ "name": "Start Debug",
+ "configurations": [
+ "Run Dev Server",
+ "Attach API debugger",
+ "Launch Web debugger"
+ ],
+ "stopAll": true
+ }
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/settings.json b/packages/create-cedar-app/esm-templates/ts/.vscode/settings.json
new file mode 100644
index 0000000000..6887d360eb
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "editor.tabSize": 2,
+ "files.trimTrailingWhitespace": true,
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit"
+ },
+ "[prisma]": {
+ "editor.formatOnSave": true
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json b/packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json
new file mode 100644
index 0000000000..549249ec63
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json
@@ -0,0 +1,29 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "WaitForDevServer",
+ "group": "none",
+ "type": "shell",
+ "command": "bash",
+ "args": [
+ "-c",
+ "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;"
+ ],
+ "windows": {
+ "command": "powershell",
+ "args": [
+ "-NoProfile",
+ "-ExecutionPolicy", "Bypass",
+ "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };"
+ ]
+ },
+ "presentation": {
+ "reveal": "silent",
+ "revealProblems": "onProblem",
+ "panel": "shared",
+ "close": true
+ }
+ },
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/.yarnrc.yml b/packages/create-cedar-app/esm-templates/ts/.yarnrc.yml
new file mode 100644
index 0000000000..e8c5d50aa7
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/.yarnrc.yml
@@ -0,0 +1,15 @@
+# Yarn's manifest file. You can configure yarn here.
+# See https://yarnpkg.com/configuration/yarnrc.
+
+# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option.
+compressionLevel: 0
+
+enableGlobalCache: true
+
+# Lets yarn use hardlinks inside `node_modules` to dedupe packages.
+# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store.
+nmMode: hardlinks-local
+
+# How to install Node packages.
+# Heads up: right now, Redwood expects this to be `node-modules`.
+nodeLinker: node-modules
diff --git a/packages/create-cedar-app/esm-templates/ts/README.md b/packages/create-cedar-app/esm-templates/ts/README.md
new file mode 100644
index 0000000000..4487e7f63d
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/README.md
@@ -0,0 +1,17 @@
+# README
+
+Welcome to your new [CedarJS](https://cedarjs.com) project!
+
+Start by installing dependencies:
+
+```
+yarn install
+```
+
+Then start the development server:
+
+```
+yarn redwood dev
+```
+
+Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources.
diff --git a/packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma b/packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma
new file mode 100644
index 0000000000..8c8666606f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma
@@ -0,0 +1,24 @@
+// Don't forget to tell Prisma about your edits to this file using
+// `yarn rw prisma migrate dev` or `yarn rw prisma db push`.
+// `migrate` is like committing while `push` is for prototyping.
+// Read more about both here:
+// https://www.prisma.io/docs/orm/prisma-migrate
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = "native"
+}
+
+// Define your own datamodels here and run `yarn redwood prisma migrate dev`
+// to create migrations for them and apply to your dev DB.
+// TODO: Please remove the following example:
+model UserExample {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/api/package.json b/packages/create-cedar-app/esm-templates/ts/api/package.json
new file mode 100644
index 0000000000..f5f58e869c
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "api",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@cedarjs/api": "0.0.5",
+ "@cedarjs/graphql-server": "0.0.5"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts b/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts
new file mode 100644
index 0000000000..1de4ac9b65
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts
@@ -0,0 +1,18 @@
+import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
+
+import requireAuth from './requireAuth.js'
+
+describe('requireAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(requireAuth.schema).toBeTruthy()
+ expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
+ })
+
+ it('requireAuth has stub implementation. Should not throw when current user', () => {
+ // If you want to set values in context, pass it through e.g.
+ // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
+ const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
+
+ expect(mockExecution).not.toThrowError()
+ })
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts b/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts
new file mode 100644
index 0000000000..3dadf21e68
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts
@@ -0,0 +1,25 @@
+import { gql } from 'graphql-tag'
+
+import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+import { requireAuth as applicationRequireAuth } from 'src/lib/auth.js'
+
+export const schema = gql`
+ """
+ Use to check whether or not a user is authenticated and is associated
+ with an optional set of roles.
+ """
+ directive @requireAuth(roles: [String]) on FIELD_DEFINITION
+`
+
+type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
+
+const validate: RequireAuthValidate = ({ directiveArgs }) => {
+ const { roles } = directiveArgs
+ applicationRequireAuth({ roles })
+}
+
+const requireAuth = createValidatorDirective(schema, validate)
+
+export default requireAuth
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts b/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts
new file mode 100644
index 0000000000..68c006bdae
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts
@@ -0,0 +1,10 @@
+import { getDirectiveName } from '@cedarjs/testing/api'
+
+import skipAuth from './skipAuth.js'
+
+describe('skipAuth directive', () => {
+ it('declares the directive sdl as schema, with the correct name', () => {
+ expect(skipAuth.schema).toBeTruthy()
+ expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
+ })
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts b/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts
new file mode 100644
index 0000000000..fdea5cf17b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts
@@ -0,0 +1,16 @@
+import { gql } from 'graphql-tag'
+
+import { createValidatorDirective } from '@cedarjs/graphql-server'
+
+export const schema = gql`
+ """
+ Use to skip authentication checks and allow public access.
+ """
+ directive @skipAuth on FIELD_DEFINITION
+`
+
+const skipAuth = createValidatorDirective(schema, () => {
+ return
+})
+
+export default skipAuth
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts b/packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts
new file mode 100644
index 0000000000..1cbf10059a
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts
@@ -0,0 +1,19 @@
+import { createGraphQLHandler } from '@cedarjs/graphql-server'
+
+import directives from 'src/directives/**/*.{js,ts}'
+import sdls from 'src/graphql/**/*.sdl.{js,ts}'
+import services from 'src/services/**/*.{js,ts}'
+
+import { db } from 'src/lib/db.js'
+import { logger } from 'src/lib/logger.js'
+
+export const handler = createGraphQLHandler({
+ loggerConfig: { logger, options: {} },
+ directives,
+ sdls,
+ services,
+ onException: () => {
+ // Disconnect from your database with an unhandled exception.
+ db.$disconnect()
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/graphql/.keep b/packages/create-cedar-app/esm-templates/ts/api/src/graphql/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts b/packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts
new file mode 100644
index 0000000000..ef6d8c9d10
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts
@@ -0,0 +1,32 @@
+/**
+ * Once you are ready to add authentication to your application
+ * you'll build out requireAuth() with real functionality. For
+ * now we just return `true` so that the calls in services
+ * have something to check against, simulating a logged
+ * in user that is allowed to access that service.
+ *
+ * See https://redwoodjs.com/docs/authentication for more info.
+ */
+export const isAuthenticated = () => {
+ return true
+}
+
+export const hasRole = ({ roles }) => {
+ return roles !== undefined
+}
+
+// This is used by the redwood directive
+// in ./api/src/directives/requireAuth
+
+// Roles are passed in by the requireAuth directive if you have auth setup
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const requireAuth = ({ roles }) => {
+ return isAuthenticated()
+}
+
+export const getCurrentUser = async () => {
+ throw new Error(
+ 'Auth is not set up yet. See https://redwoodjs.com/docs/authentication ' +
+ 'to get started'
+ )
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts b/packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts
new file mode 100644
index 0000000000..b6fa93228f
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts
@@ -0,0 +1,26 @@
+// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor
+// for options.
+
+import { PrismaClient } from '@prisma/client'
+
+import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger'
+
+import { logger } from './logger.js'
+
+const prismaClient = new PrismaClient({
+ log: emitLogLevels(['info', 'warn', 'error']),
+})
+
+handlePrismaLogging({
+ db: prismaClient,
+ logger,
+ logLevels: ['info', 'warn', 'error'],
+})
+
+/**
+ * Global Prisma client extensions should be added here, as $extend
+ * returns a new instance.
+ * export const db = prismaClient.$extend(...)
+ * Add any .$on hooks before using $extend
+ */
+export const db = prismaClient
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts b/packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts
new file mode 100644
index 0000000000..b5f38c1921
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts
@@ -0,0 +1,17 @@
+import { createLogger } from '@cedarjs/api/logger'
+
+/**
+ * Creates a logger with RedwoodLoggerOptions
+ *
+ * These extend and override default LoggerOptions,
+ * can define a destination like a file or other supported pino log transport stream,
+ * and sets whether or not to show the logger configuration settings (defaults to false)
+ *
+ * @param RedwoodLoggerOptions
+ *
+ * RedwoodLoggerOptions have
+ * @param {options} LoggerOptions - defines how to log, such as redaction and format
+ * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file
+ * @param {boolean} showConfig - whether to display logger configuration on initialization
+ */
+export const logger = createLogger({})
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/services/.keep b/packages/create-cedar-app/esm-templates/ts/api/src/services/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/api/tsconfig.json b/packages/create-cedar-app/esm-templates/ts/api/tsconfig.json
new file mode 100644
index 0000000000..c9075d6fac
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "skipLibCheck": false,
+ "rootDirs": ["./src", "../.redwood/types/mirror/api/src"],
+ "paths": {
+ "src/*": ["./src/*", "../.redwood/types/mirror/api/src/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/api"]
+ },
+ "typeRoots": ["../node_modules/@types", "./node_modules/@types"],
+ "types": ["jest"],
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/api-*",
+ "../types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts b/packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts
new file mode 100644
index 0000000000..f3de348f75
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config'
+
+import { cedarVitestPreset } from '@cedarjs/vite/api'
+
+export default defineConfig({
+ plugins: [cedarVitestPreset()],
+ test: {
+ globals: true,
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/gitignore.template b/packages/create-cedar-app/esm-templates/ts/gitignore.template
new file mode 100644
index 0000000000..31d9637ede
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/gitignore.template
@@ -0,0 +1,24 @@
+.idea
+.DS_Store
+.env*
+!.env.example
+!.env.defaults
+.netlify
+.redwood/*
+!.redwood/README.md
+dev.db*
+dist
+dist-babel
+node_modules
+yarn-error.log
+web/public/mockServiceWorker.js
+web/types/graphql.d.ts
+api/types/graphql.d.ts
+api/src/lib/generateGraphiQLHeader.*
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
diff --git a/packages/create-cedar-app/esm-templates/ts/graphql.config.cjs b/packages/create-cedar-app/esm-templates/ts/graphql.config.cjs
new file mode 100644
index 0000000000..d82bff34e0
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/graphql.config.cjs
@@ -0,0 +1,11 @@
+// This file is used by the VSCode GraphQL extension
+
+const { getPaths } = require('@cedarjs/project-config')
+
+/** @type {import('graphql-config').IGraphQLConfig} */
+const config = {
+ schema: getPaths().generated.schema,
+ documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}',
+}
+
+module.exports = config
diff --git a/packages/create-cedar-app/esm-templates/ts/package.json b/packages/create-cedar-app/esm-templates/ts/package.json
new file mode 100644
index 0000000000..e403c327ae
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/package.json
@@ -0,0 +1,32 @@
+{
+ "private": true,
+ "type": "module",
+ "workspaces": {
+ "packages": [
+ "api",
+ "web"
+ ]
+ },
+ "devDependencies": {
+ "@cedarjs/core": "0.0.5",
+ "@cedarjs/project-config": "0.0.5",
+ "@cedarjs/testing": "0.0.5",
+ "vitest": "3.2.4"
+ },
+ "eslintConfig": {
+ "extends": "@cedarjs/eslint-config",
+ "root": true
+ },
+ "engines": {
+ "node": "=20.x"
+ },
+ "prisma": {
+ "seed": "yarn rw exec seed"
+ },
+ "packageManager": "yarn@4.9.2",
+ "resolutions": {
+ "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
+ "react-is": "19.0.0-rc-f2df5694-20240916",
+ "vite": "5.4.16"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/prettier.config.cjs b/packages/create-cedar-app/esm-templates/ts/prettier.config.cjs
new file mode 100644
index 0000000000..45058f7aa2
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/prettier.config.cjs
@@ -0,0 +1,18 @@
+// https://prettier.io/docs/en/options.html
+/** @type {import('prettier').RequiredOptions} */
+module.exports = {
+ trailingComma: 'es5',
+ bracketSpacing: true,
+ tabWidth: 2,
+ semi: false,
+ singleQuote: true,
+ arrowParens: 'always',
+ overrides: [
+ {
+ files: 'Routes.*',
+ options: {
+ printWidth: 999,
+ },
+ },
+ ],
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/redwood.toml b/packages/create-cedar-app/esm-templates/ts/redwood.toml
new file mode 100644
index 0000000000..c2c2be24d6
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/redwood.toml
@@ -0,0 +1,21 @@
+# This file contains the configuration settings for your Redwood app.
+# This file is also what makes your Redwood app a Redwood app.
+# If you remove it and try to run `yarn rw dev`, you'll get an error.
+#
+# For the full list of options, see the "App Configuration: redwood.toml" doc:
+# https://redwoodjs.com/docs/app-configuration-redwood-toml
+
+[web]
+ title = "Cedar App"
+ port = 8910
+ apiUrl = "/.redwood/functions"
+ includeEnvironmentVariables = [
+ # Add any ENV vars that should be available to the web side to this array
+ # See https://redwoodjs.com/docs/environment-variables#web
+ ]
+[api]
+ port = 8911
+[browser]
+ open = true
+[notifications]
+ versionUpdates = ["latest"]
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/.keep b/packages/create-cedar-app/esm-templates/ts/scripts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/seed.ts b/packages/create-cedar-app/esm-templates/ts/scripts/seed.ts
new file mode 100644
index 0000000000..080e6d1d5a
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/scripts/seed.ts
@@ -0,0 +1,27 @@
+// import { db } from 'api/src/lib/db.js'
+
+// Manually apply seeds via the `yarn rw prisma db seed` command.
+//
+// Seeds automatically run the first time you run the `yarn rw prisma migrate dev`
+// command and every time you run the `yarn rw prisma migrate reset` command.
+//
+// See https://redwoodjs.com/docs/database-seeds for more info
+
+export default async () => {
+ try {
+ // Create your database records here! For example, seed some users:
+ //
+ // const users = [
+ // { name: 'Alice', email: 'alice@cedarjs.com' },
+ // { name: 'Bob', email: 'bob@cedarjs.com' },
+ // ]
+ //
+ // await db.user.createMany({ data: users })
+
+ console.info(
+ '\n No seed data, skipping. See scripts/seed.ts to start seeding your database!\n'
+ )
+ } catch (error) {
+ console.error(error)
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json b/packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json
new file mode 100644
index 0000000000..5690eec16c
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2023",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "paths": {
+ "$api/*": ["../api/*"],
+ "api/*": ["../api/*"],
+ "$api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"],
+ "$web/*": ["../web/*"],
+ "web/*": ["../web/*"],
+ "$web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"],
+ "types/*": ["../types/*", "../web/types/*", "../api/types/*"]
+ },
+ "typeRoots": ["../node_modules/@types"],
+ "jsx": "preserve"
+ },
+ "include": [
+ ".",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/vitest.config.ts b/packages/create-cedar-app/esm-templates/ts/vitest.config.ts
new file mode 100644
index 0000000000..6c64e7920b
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: ['/{*,!(node_modules)/**/}/vite?(st).config.{js,ts}'],
+ },
+})
diff --git a/packages/create-cedar-app/esm-templates/ts/web/package.json b/packages/create-cedar-app/esm-templates/ts/web/package.json
new file mode 100644
index 0000000000..23c0584006
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "web",
+ "type": "module",
+ "version": "0.0.0",
+ "private": true,
+ "browserslist": {
+ "development": [
+ "last 1 version"
+ ],
+ "production": [
+ "defaults"
+ ]
+ },
+ "dependencies": {
+ "@cedarjs/forms": "0.0.5",
+ "@cedarjs/router": "0.0.5",
+ "@cedarjs/web": "0.0.5",
+ "react": "19.0.0-rc-f2df5694-20240916",
+ "react-dom": "19.0.0-rc-f2df5694-20240916"
+ },
+ "devDependencies": {
+ "@cedarjs/vite": "0.0.5",
+ "@types/react": "^18.2.55",
+ "@types/react-dom": "^18.2.19"
+ }
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/web/public/README.md b/packages/create-cedar-app/esm-templates/ts/web/public/README.md
new file mode 100644
index 0000000000..1b09bf8361
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/public/README.md
@@ -0,0 +1,43 @@
+# Static Assets
+
+Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`.
+
+> Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes.
+
+### Example Use
+
+A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g.
+
+```
+
+```
+
+and
+
+```
+ alt="Logo" />
+```
+
+## Best Practices
+
+Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc.
+
+In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash.
+
+### Example Asset Import with Vite
+
+Instead of handling our logo image as a static file per the example above, we can do the following:
+
+```
+import React from "react"
+import logo from "./my-logo.jpg"
+
+
+function Header() {
+ return
+}
+
+export default Header
+```
+
+See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html)
diff --git a/packages/create-cedar-app/esm-templates/ts/web/public/favicon.png b/packages/create-cedar-app/esm-templates/ts/web/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1232ba73ab4eaff34dd96755634fb0c5f8e2267f
GIT binary patch
literal 2500
zcmZ`*2{_bi7k_6g4VfZYQZ#Wbam|cjEMsQu8p~X33>VE{(#R~$j3OW?A$pCG1({wssjLJCzgI-$VrV66kjR;I}HFxOa|aRq)L1aKr9x3
z5jp^bOaL@^;_E&{$Z(ZObK6X%0%J%<06CZzkVb$(Ta;Hq
zGXD?@$U8B9wkTsz*1Ey!9bQ6Di6+Q`O}0n
zswW|W%SJOIx#3K79GfRa0b(2h64^{40~N=P;RpzEB;7?10whb-7+ut&i*PGRmqztQ
zIdS<+lqDLA#_Ez4Q79CVAGw*}>+Jdo4(&*~TZBR$0fX7GV+VSNIhxCl!kF3E*kG_Y
z3=U@sd6){~IYLI9DMz69G05jQ&P+iBpT!fhxEz!;E+d>9DSHn@bTkAfJ~SvU-=E8kA^)Gz5i#F8
zel2)029+Dh+8po95HiWqDVt&~O|e$~pQrp0{*BK!XlY%9AYZ0{8xt!%7P1wNi1{Do
z_W&m@o5yDg1X2M0lmEB6@BL~2l;B(CCjb#6oys>;`gjEvwa^_PD?+V&zE5Pulec_^
z0gwl7&JO-Gh3dc{ia2WJoa+Yd*!tZ)({jc22eu_Yj#klA+t03@bScy?lw%$7Za!r-
z>s^3CL+~p+O~Z6}c({9ifBz`#@PlC6D{%Y17cXXW^7He@rl*_Y
zUx5{>sy&laQ_JkgWc9qfyw`ti2P_ur9KpusNLrd|-^fT-@960KK|JcR`@W&8trZ%&d!#b+H!e!bjaS8aZ9BwckkZS(R<&ddkd~@S63%HcUIfl+E&R9=n@FQk~5NWl%_YPzP>&=>`Dc4*|I(GuF(u|
zHFIB0O^t(f=b;WE?;>N=jii0^5-m9R6w4Z_OG`%wos*R{^1E!Jix$Pr%`JH*=aRoM
z^4{p!n0I4SlLxY+tII=sjBEl28^glF?0SQHp!46Qia4f8qtUPuUS@W7=nAdlLRswo
zs#cjb+u<>=Oe2!RCK0$N`ZiTt+5sU1`O|`J^P7hb=97cr2NW_H#hh
z*W~&e2O;->N^5g-^P|&V_hCMn3A{Z$MU|B^6RoYSnA}OkQEpV!h743THhKzh*0{eE
z(cKf*pV2AJK6`d`aY;$a?NjF0ng@r2k-1Om>+0(Ru9TGAiQXr1rqMDxo;*=&B)K%a
z8Nk0mPfbr38U?9&-ma^wd)C|A8~9ADdaqoI78cfSg~yW)R<`DbdW<}jnK!i!Q$^V7
zYQI@SmlM0I(>JNDTv;IFhrF21Bv?FY#DT}EI2?|AR3!Ry`AyhL4LxQ_TJRvP2#zW=
zx&)h4sNK4D?b=JKc6N4UWo0UgbCF6)N|RZH;qoB))LC>%z}aJje_w59v)L~8_KC8!
zYXpXKqO3N_)(k_uB-T^LBdrN$d7-4_Rl-b{nH)nMM9(N>-4`_PT{0QZkc{2i@GSGB
zZ*q+NjCyMSKuGU{=T=r$h4y#kg?y2JHO1YXe_M%>@U#lie?8#Ox&zqaBF685K@*;G
z`4Mv1{$GAkGY-w|9u!#Wxl$#>^Pwg!EiF9IZao!%t0PWO3?mhjJaSaQ8M^Y!0_s=U%iV_(^{_$Z!?GbT
z`toF$=-^55-toKTF_uE-2n$cDvN9}HVM^Zh-m2O+k*5px(9FbZl}A?PtTr$-+-pG?
z9$z6dG&Hoysl?bd<%j_?@qH+^-_y`@$@Olf^RYKbUdwbXjPEFBYn4l;gYYkr;+z;d
z5{Y~_H#g_iCpdRJrAr_1ly5X7hV^uG%;@i&bx25v
zT~0_o3}5x`8hX9~dInsJ(?w#sh-LrSV4Xz8x_VHI^1P7ew|)C|zCbYHLF^iWa~if9
zYg)UG&c7c%u58rOGxpj@!+cYQuECJi#mtlsA3hi>mgeV2&@QyxO2s=r4e|4vI6nU5
z*liD`-dY`TMM#KbqO`Qs (
+
+
+ {children}
+
+
+)
+
+export default App
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx b/packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx
new file mode 100644
index 0000000000..ae5daa0c22
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx
@@ -0,0 +1,20 @@
+// In this file, all Page components from 'src/pages` are auto-imported. Nested
+// directories are supported, and should be uppercase. Each subdirectory will be
+// prepended onto the component name.
+//
+// Examples:
+//
+// 'src/pages/HomePage/HomePage.js' -> HomePage
+// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
+
+import { Router, Route } from '@cedarjs/router'
+
+const Routes = () => {
+ return (
+
+
+
+ )
+}
+
+export default Routes
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/components/.keep b/packages/create-cedar-app/esm-templates/ts/web/src/components/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx b/packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx
new file mode 100644
index 0000000000..915c14d76d
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx
@@ -0,0 +1,35 @@
+import { hydrateRoot, createRoot } from 'react-dom/client'
+
+import App from './App'
+import Routes from './Routes'
+
+/**
+ * When `#redwood-app` isn't empty then it's very likely that you're using
+ * prerendering. So React attaches event listeners to the existing markup
+ * rather than replacing it.
+ * https://react.dev/reference/react-dom/client/hydrateRoot
+ */
+const redwoodAppElement = document.getElementById('redwood-app')
+
+if (!redwoodAppElement) {
+ throw new Error(
+ "Could not find an element with ID 'redwood-app'. Please ensure it " +
+ "exists in your 'web/src/index.html' file."
+ )
+}
+
+if (redwoodAppElement.children?.length > 0) {
+ hydrateRoot(
+ redwoodAppElement,
+
+
+
+ )
+} else {
+ const root = createRoot(redwoodAppElement)
+ root.render(
+
+
+
+ )
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/index.css b/packages/create-cedar-app/esm-templates/ts/web/src/index.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/index.html b/packages/create-cedar-app/esm-templates/ts/web/src/index.html
new file mode 100644
index 0000000000..e240b8eb8c
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/layouts/.keep b/packages/create-cedar-app/esm-templates/ts/web/src/layouts/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx b/packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
new file mode 100644
index 0000000000..8afcee8498
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
@@ -0,0 +1,57 @@
+// This page will be rendered when an error makes it all the way to the top of the
+// application without being handled by a Javascript catch statement or React error
+// boundary.
+//
+// You can modify this page as you wish, but it is important to keep things simple to
+// avoid the possibility that it will cause its own error. If it does, Redwood will
+// still render a generic error page, but your users will prefer something a bit more
+// thoughtful :)
+
+// This import will be automatically removed when building for production
+import { DevFatalErrorPage } from '@cedarjs/web/dist/components/DevFatalErrorPage'
+
+export default DevFatalErrorPage ||
+ (() => (
+
+
+
+
+ Something went wrong
+
+
+
+ ))
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx b/packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
new file mode 100644
index 0000000000..92ef916989
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
@@ -0,0 +1,44 @@
+export default () => (
+
+
+
+
+ 404 Page Not Found
+
+
+
+)
diff --git a/packages/create-cedar-app/esm-templates/ts/web/tsconfig.json b/packages/create-cedar-app/esm-templates/ts/web/tsconfig.json
new file mode 100644
index 0000000000..edfcaff396
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "noEmit": true,
+ "allowJs": true,
+ "esModuleInterop": true,
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "skipLibCheck": false,
+ "rootDirs": [
+ "./src",
+ "../.redwood/types/mirror/web/src",
+ "../api/src",
+ "../.redwood/types/mirror/api/src"
+ ],
+ "paths": {
+ "src/*": [
+ "./src/*",
+ "../.redwood/types/mirror/web/src/*",
+ "../api/src/*",
+ "../.redwood/types/mirror/api/src/*"
+ ],
+ "$api/*": ["../api/*"],
+ "types/*": ["./types/*", "../types/*"],
+ "@cedarjs/testing": ["../node_modules/@cedarjs/testing/web"]
+ },
+ "typeRoots": [
+ "../node_modules/@types",
+ "./node_modules/@types",
+ "../node_modules/@testing-library"
+ ],
+ "types": ["jest", "jest-dom"],
+ "jsx": "preserve"
+ },
+ "include": [
+ "src",
+ "config",
+ ".storybook/**/*",
+ "../.redwood/types/includes/all-*",
+ "../.redwood/types/includes/web-*",
+ "../types",
+ "./types"
+ ]
+}
diff --git a/packages/create-cedar-app/esm-templates/ts/web/vite.config.ts b/packages/create-cedar-app/esm-templates/ts/web/vite.config.ts
new file mode 100644
index 0000000000..8db6f5be72
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/vite.config.ts
@@ -0,0 +1,21 @@
+///
+
+import dns from 'node:dns'
+
+import { defineConfig } from 'vite'
+
+import { cedar } from '@cedarjs/vite'
+
+// So that Vite will load on localhost instead of `127.0.0.1`.
+// See: https://vitejs.dev/config/server-options.html#server-host.
+dns.setDefaultResultOrder('verbatim')
+
+export default defineConfig(({ mode }) => ({
+ plugins: [cedar({ mode })],
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./vitest.setup.ts'],
+ // Enables global test APIs like describe, it, expect
+ globals: true,
+ },
+}))
diff --git a/packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts b/packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts
new file mode 100644
index 0000000000..3c2a688f47
--- /dev/null
+++ b/packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts
@@ -0,0 +1,12 @@
+import '@testing-library/jest-dom/vitest'
+
+import { cleanup } from '@testing-library/react'
+import { afterEach } from 'vitest'
+
+afterEach(() => {
+ // If vitest globals are enabled testing-library will clean up after each
+ // test automatically, but we don't enable globals, so we have to manually
+ // clean up here
+ // https://testing-library.com/docs/react-testing-library/api/#cleanup
+ cleanup()
+})
From f6ee965fd862585bd885b132832c9afdc78361ea Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 9 Aug 2025 12:58:08 +0200
Subject: [PATCH 3/5] add esm-templates to .prettierignore
---
.prettierignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.prettierignore b/.prettierignore
index d0796cc03c..8465677796 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -20,6 +20,7 @@ packages/testing/config
# Ignore create-cedar-app (we don't want to mismatch between framework and project)
/packages/create-cedar-app/templates
+/packages/create-cedar-app/esm-templates
/packages/create-cedar-app/tests/**/*.sh
# Ignore create-cedar-rsc-app (it has it's own config)
From 41dc3d28076ee269a0bf474f907c95c35ee7e810 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 9 Aug 2025 13:17:48 +0200
Subject: [PATCH 4/5] exclude esm-templates
---
packages/create-cedar-app/vitest.config.mts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/create-cedar-app/vitest.config.mts b/packages/create-cedar-app/vitest.config.mts
index 6d9f922467..377b00f059 100644
--- a/packages/create-cedar-app/vitest.config.mts
+++ b/packages/create-cedar-app/vitest.config.mts
@@ -2,6 +2,6 @@ import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
- exclude: [...configDefaults.exclude, 'templates/**'],
+ exclude: [...configDefaults.exclude, 'templates/**', 'esm-templates/**'],
},
})
From dd4b7a3643ed369129a4d2c8df63710d504e1383 Mon Sep 17 00:00:00 2001
From: Tobbe Lundberg
Date: Sat, 9 Aug 2025 13:39:29 +0200
Subject: [PATCH 5/5] move templates
---
.prettierignore | 1 -
.../js => templates/esm-js}/.editorconfig | 0
.../{esm-templates/js => templates/esm-js}/.env | 0
.../js => templates/esm-js}/.env.defaults | 0
.../js => templates/esm-js}/.env.example | 0
.../js => templates/esm-js}/.redwood/README.md | 0
.../js => templates/esm-js}/.vscode/extensions.json | 0
.../js => templates/esm-js}/.vscode/launch.json | 0
.../js => templates/esm-js}/.vscode/settings.json | 0
.../js => templates/esm-js}/.vscode/tasks.json | 0
.../js => templates/esm-js}/.yarnrc.yml | 0
.../js => templates/esm-js}/README.md | 0
.../js => templates/esm-js}/api/db/schema.prisma | 0
.../js => templates/esm-js}/api/jsconfig.json | 0
.../js => templates/esm-js}/api/package.json | 0
.../api/src/directives/requireAuth/requireAuth.js | 0
.../src/directives/requireAuth/requireAuth.test.js | 0
.../esm-js}/api/src/directives/skipAuth/skipAuth.js | 0
.../api/src/directives/skipAuth/skipAuth.test.js | 0
.../esm-js}/api/src/functions/graphql.js | 0
.../js => templates/esm-js}/api/src/graphql/.keep | 0
.../js => templates/esm-js}/api/src/lib/auth.js | 0
.../js => templates/esm-js}/api/src/lib/db.js | 0
.../js => templates/esm-js}/api/src/lib/logger.js | 0
.../js => templates/esm-js}/api/src/services/.keep | 0
.../js => templates/esm-js}/api/vitest.config.js | 0
.../js => templates/esm-js}/gitignore.template | 0
.../js => templates/esm-js}/graphql.config.cjs | 0
.../js => templates/esm-js}/package.json | 0
.../js => templates/esm-js}/prettier.config.cjs | 0
.../js => templates/esm-js}/redwood.toml | 0
.../js => templates/esm-js}/scripts/.keep | 0
.../js => templates/esm-js}/scripts/jsconfig.json | 0
.../js => templates/esm-js}/scripts/seed.js | 0
.../js => templates/esm-js}/vitest.config.mjs | 0
.../js => templates/esm-js}/web/jsconfig.json | 0
.../js => templates/esm-js}/web/package.json | 0
.../js => templates/esm-js}/web/public/README.md | 0
.../js => templates/esm-js}/web/public/favicon.png | Bin
.../js => templates/esm-js}/web/public/robots.txt | 0
.../js => templates/esm-js}/web/src/App.jsx | 0
.../js => templates/esm-js}/web/src/Routes.jsx | 0
.../esm-js}/web/src/components/.keep | 0
.../esm-js}/web/src/entry.client.jsx | 0
.../js => templates/esm-js}/web/src/index.css | 0
.../js => templates/esm-js}/web/src/index.html | 0
.../js => templates/esm-js}/web/src/layouts/.keep | 0
.../web/src/pages/FatalErrorPage/FatalErrorPage.jsx | 0
.../web/src/pages/NotFoundPage/NotFoundPage.jsx | 0
.../js => templates/esm-js}/web/vite.config.js | 0
.../js => templates/esm-js}/web/vitest.setup.js | 0
.../ts => templates/esm-ts}/.editorconfig | 0
.../{esm-templates/ts => templates/esm-ts}/.env | 0
.../ts => templates/esm-ts}/.env.defaults | 0
.../ts => templates/esm-ts}/.env.example | 0
.../ts => templates/esm-ts}/.redwood/README.md | 0
.../ts => templates/esm-ts}/.vscode/extensions.json | 0
.../ts => templates/esm-ts}/.vscode/launch.json | 0
.../ts => templates/esm-ts}/.vscode/settings.json | 0
.../ts => templates/esm-ts}/.vscode/tasks.json | 0
.../ts => templates/esm-ts}/.yarnrc.yml | 0
.../ts => templates/esm-ts}/README.md | 0
.../ts => templates/esm-ts}/api/db/schema.prisma | 0
.../ts => templates/esm-ts}/api/package.json | 0
.../src/directives/requireAuth/requireAuth.test.ts | 0
.../api/src/directives/requireAuth/requireAuth.ts | 0
.../api/src/directives/skipAuth/skipAuth.test.ts | 0
.../esm-ts}/api/src/directives/skipAuth/skipAuth.ts | 0
.../esm-ts}/api/src/functions/graphql.ts | 0
.../ts => templates/esm-ts}/api/src/graphql/.keep | 0
.../ts => templates/esm-ts}/api/src/lib/auth.ts | 0
.../ts => templates/esm-ts}/api/src/lib/db.ts | 0
.../ts => templates/esm-ts}/api/src/lib/logger.ts | 0
.../ts => templates/esm-ts}/api/src/services/.keep | 0
.../ts => templates/esm-ts}/api/tsconfig.json | 0
.../ts => templates/esm-ts}/api/vitest.config.ts | 0
.../ts => templates/esm-ts}/gitignore.template | 0
.../ts => templates/esm-ts}/graphql.config.cjs | 0
.../ts => templates/esm-ts}/package.json | 0
.../ts => templates/esm-ts}/prettier.config.cjs | 0
.../ts => templates/esm-ts}/redwood.toml | 0
.../ts => templates/esm-ts}/scripts/.keep | 0
.../ts => templates/esm-ts}/scripts/seed.ts | 0
.../ts => templates/esm-ts}/scripts/tsconfig.json | 0
.../ts => templates/esm-ts}/vitest.config.ts | 0
.../ts => templates/esm-ts}/web/package.json | 0
.../ts => templates/esm-ts}/web/public/README.md | 0
.../ts => templates/esm-ts}/web/public/favicon.png | Bin
.../ts => templates/esm-ts}/web/public/robots.txt | 0
.../ts => templates/esm-ts}/web/src/App.tsx | 0
.../ts => templates/esm-ts}/web/src/Routes.tsx | 0
.../esm-ts}/web/src/components/.keep | 0
.../esm-ts}/web/src/entry.client.tsx | 0
.../ts => templates/esm-ts}/web/src/index.css | 0
.../ts => templates/esm-ts}/web/src/index.html | 0
.../ts => templates/esm-ts}/web/src/layouts/.keep | 0
.../web/src/pages/FatalErrorPage/FatalErrorPage.tsx | 0
.../web/src/pages/NotFoundPage/NotFoundPage.tsx | 0
.../ts => templates/esm-ts}/web/tsconfig.json | 0
.../ts => templates/esm-ts}/web/vite.config.ts | 0
.../ts => templates/esm-ts}/web/vitest.setup.ts | 0
packages/create-cedar-app/vitest.config.mts | 2 +-
102 files changed, 1 insertion(+), 2 deletions(-)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.editorconfig (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.env (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.env.defaults (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.env.example (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.redwood/README.md (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.vscode/extensions.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.vscode/launch.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.vscode/settings.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.vscode/tasks.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/.yarnrc.yml (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/README.md (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/db/schema.prisma (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/jsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/package.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/directives/requireAuth/requireAuth.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/directives/requireAuth/requireAuth.test.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/directives/skipAuth/skipAuth.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/directives/skipAuth/skipAuth.test.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/functions/graphql.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/graphql/.keep (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/lib/auth.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/lib/db.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/lib/logger.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/src/services/.keep (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/api/vitest.config.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/gitignore.template (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/graphql.config.cjs (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/package.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/prettier.config.cjs (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/redwood.toml (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/scripts/.keep (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/scripts/jsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/scripts/seed.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/vitest.config.mjs (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/jsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/package.json (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/public/README.md (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/public/favicon.png (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/public/robots.txt (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/App.jsx (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/Routes.jsx (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/components/.keep (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/entry.client.jsx (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/index.css (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/index.html (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/layouts/.keep (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/pages/FatalErrorPage/FatalErrorPage.jsx (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/src/pages/NotFoundPage/NotFoundPage.jsx (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/vite.config.js (100%)
rename packages/create-cedar-app/{esm-templates/js => templates/esm-js}/web/vitest.setup.js (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.editorconfig (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.env (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.env.defaults (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.env.example (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.redwood/README.md (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.vscode/extensions.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.vscode/launch.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.vscode/settings.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.vscode/tasks.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/.yarnrc.yml (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/README.md (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/db/schema.prisma (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/package.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/directives/requireAuth/requireAuth.test.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/directives/requireAuth/requireAuth.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/directives/skipAuth/skipAuth.test.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/directives/skipAuth/skipAuth.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/functions/graphql.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/graphql/.keep (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/lib/auth.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/lib/db.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/lib/logger.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/src/services/.keep (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/tsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/api/vitest.config.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/gitignore.template (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/graphql.config.cjs (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/package.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/prettier.config.cjs (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/redwood.toml (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/scripts/.keep (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/scripts/seed.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/scripts/tsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/vitest.config.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/package.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/public/README.md (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/public/favicon.png (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/public/robots.txt (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/App.tsx (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/Routes.tsx (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/components/.keep (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/entry.client.tsx (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/index.css (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/index.html (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/layouts/.keep (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/pages/FatalErrorPage/FatalErrorPage.tsx (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/src/pages/NotFoundPage/NotFoundPage.tsx (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/tsconfig.json (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/vite.config.ts (100%)
rename packages/create-cedar-app/{esm-templates/ts => templates/esm-ts}/web/vitest.setup.ts (100%)
diff --git a/.prettierignore b/.prettierignore
index 8465677796..d0796cc03c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -20,7 +20,6 @@ packages/testing/config
# Ignore create-cedar-app (we don't want to mismatch between framework and project)
/packages/create-cedar-app/templates
-/packages/create-cedar-app/esm-templates
/packages/create-cedar-app/tests/**/*.sh
# Ignore create-cedar-rsc-app (it has it's own config)
diff --git a/packages/create-cedar-app/esm-templates/js/.editorconfig b/packages/create-cedar-app/templates/esm-js/.editorconfig
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.editorconfig
rename to packages/create-cedar-app/templates/esm-js/.editorconfig
diff --git a/packages/create-cedar-app/esm-templates/js/.env b/packages/create-cedar-app/templates/esm-js/.env
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.env
rename to packages/create-cedar-app/templates/esm-js/.env
diff --git a/packages/create-cedar-app/esm-templates/js/.env.defaults b/packages/create-cedar-app/templates/esm-js/.env.defaults
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.env.defaults
rename to packages/create-cedar-app/templates/esm-js/.env.defaults
diff --git a/packages/create-cedar-app/esm-templates/js/.env.example b/packages/create-cedar-app/templates/esm-js/.env.example
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.env.example
rename to packages/create-cedar-app/templates/esm-js/.env.example
diff --git a/packages/create-cedar-app/esm-templates/js/.redwood/README.md b/packages/create-cedar-app/templates/esm-js/.redwood/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.redwood/README.md
rename to packages/create-cedar-app/templates/esm-js/.redwood/README.md
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/extensions.json b/packages/create-cedar-app/templates/esm-js/.vscode/extensions.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.vscode/extensions.json
rename to packages/create-cedar-app/templates/esm-js/.vscode/extensions.json
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/launch.json b/packages/create-cedar-app/templates/esm-js/.vscode/launch.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.vscode/launch.json
rename to packages/create-cedar-app/templates/esm-js/.vscode/launch.json
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/settings.json b/packages/create-cedar-app/templates/esm-js/.vscode/settings.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.vscode/settings.json
rename to packages/create-cedar-app/templates/esm-js/.vscode/settings.json
diff --git a/packages/create-cedar-app/esm-templates/js/.vscode/tasks.json b/packages/create-cedar-app/templates/esm-js/.vscode/tasks.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.vscode/tasks.json
rename to packages/create-cedar-app/templates/esm-js/.vscode/tasks.json
diff --git a/packages/create-cedar-app/esm-templates/js/.yarnrc.yml b/packages/create-cedar-app/templates/esm-js/.yarnrc.yml
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/.yarnrc.yml
rename to packages/create-cedar-app/templates/esm-js/.yarnrc.yml
diff --git a/packages/create-cedar-app/esm-templates/js/README.md b/packages/create-cedar-app/templates/esm-js/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/README.md
rename to packages/create-cedar-app/templates/esm-js/README.md
diff --git a/packages/create-cedar-app/esm-templates/js/api/db/schema.prisma b/packages/create-cedar-app/templates/esm-js/api/db/schema.prisma
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/db/schema.prisma
rename to packages/create-cedar-app/templates/esm-js/api/db/schema.prisma
diff --git a/packages/create-cedar-app/esm-templates/js/api/jsconfig.json b/packages/create-cedar-app/templates/esm-js/api/jsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/jsconfig.json
rename to packages/create-cedar-app/templates/esm-js/api/jsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/js/api/package.json b/packages/create-cedar-app/templates/esm-js/api/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/package.json
rename to packages/create-cedar-app/templates/esm-js/api/package.json
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js b/packages/create-cedar-app/templates/esm-js/api/src/directives/requireAuth/requireAuth.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.js
rename to packages/create-cedar-app/templates/esm-js/api/src/directives/requireAuth/requireAuth.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js b/packages/create-cedar-app/templates/esm-js/api/src/directives/requireAuth/requireAuth.test.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/directives/requireAuth/requireAuth.test.js
rename to packages/create-cedar-app/templates/esm-js/api/src/directives/requireAuth/requireAuth.test.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js b/packages/create-cedar-app/templates/esm-js/api/src/directives/skipAuth/skipAuth.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.js
rename to packages/create-cedar-app/templates/esm-js/api/src/directives/skipAuth/skipAuth.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js b/packages/create-cedar-app/templates/esm-js/api/src/directives/skipAuth/skipAuth.test.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/directives/skipAuth/skipAuth.test.js
rename to packages/create-cedar-app/templates/esm-js/api/src/directives/skipAuth/skipAuth.test.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js b/packages/create-cedar-app/templates/esm-js/api/src/functions/graphql.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/functions/graphql.js
rename to packages/create-cedar-app/templates/esm-js/api/src/functions/graphql.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/graphql/.keep b/packages/create-cedar-app/templates/esm-js/api/src/graphql/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/graphql/.keep
rename to packages/create-cedar-app/templates/esm-js/api/src/graphql/.keep
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js b/packages/create-cedar-app/templates/esm-js/api/src/lib/auth.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/lib/auth.js
rename to packages/create-cedar-app/templates/esm-js/api/src/lib/auth.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/db.js b/packages/create-cedar-app/templates/esm-js/api/src/lib/db.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/lib/db.js
rename to packages/create-cedar-app/templates/esm-js/api/src/lib/db.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js b/packages/create-cedar-app/templates/esm-js/api/src/lib/logger.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/lib/logger.js
rename to packages/create-cedar-app/templates/esm-js/api/src/lib/logger.js
diff --git a/packages/create-cedar-app/esm-templates/js/api/src/services/.keep b/packages/create-cedar-app/templates/esm-js/api/src/services/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/src/services/.keep
rename to packages/create-cedar-app/templates/esm-js/api/src/services/.keep
diff --git a/packages/create-cedar-app/esm-templates/js/api/vitest.config.js b/packages/create-cedar-app/templates/esm-js/api/vitest.config.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/api/vitest.config.js
rename to packages/create-cedar-app/templates/esm-js/api/vitest.config.js
diff --git a/packages/create-cedar-app/esm-templates/js/gitignore.template b/packages/create-cedar-app/templates/esm-js/gitignore.template
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/gitignore.template
rename to packages/create-cedar-app/templates/esm-js/gitignore.template
diff --git a/packages/create-cedar-app/esm-templates/js/graphql.config.cjs b/packages/create-cedar-app/templates/esm-js/graphql.config.cjs
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/graphql.config.cjs
rename to packages/create-cedar-app/templates/esm-js/graphql.config.cjs
diff --git a/packages/create-cedar-app/esm-templates/js/package.json b/packages/create-cedar-app/templates/esm-js/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/package.json
rename to packages/create-cedar-app/templates/esm-js/package.json
diff --git a/packages/create-cedar-app/esm-templates/js/prettier.config.cjs b/packages/create-cedar-app/templates/esm-js/prettier.config.cjs
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/prettier.config.cjs
rename to packages/create-cedar-app/templates/esm-js/prettier.config.cjs
diff --git a/packages/create-cedar-app/esm-templates/js/redwood.toml b/packages/create-cedar-app/templates/esm-js/redwood.toml
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/redwood.toml
rename to packages/create-cedar-app/templates/esm-js/redwood.toml
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/.keep b/packages/create-cedar-app/templates/esm-js/scripts/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/scripts/.keep
rename to packages/create-cedar-app/templates/esm-js/scripts/.keep
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json b/packages/create-cedar-app/templates/esm-js/scripts/jsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/scripts/jsconfig.json
rename to packages/create-cedar-app/templates/esm-js/scripts/jsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/js/scripts/seed.js b/packages/create-cedar-app/templates/esm-js/scripts/seed.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/scripts/seed.js
rename to packages/create-cedar-app/templates/esm-js/scripts/seed.js
diff --git a/packages/create-cedar-app/esm-templates/js/vitest.config.mjs b/packages/create-cedar-app/templates/esm-js/vitest.config.mjs
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/vitest.config.mjs
rename to packages/create-cedar-app/templates/esm-js/vitest.config.mjs
diff --git a/packages/create-cedar-app/esm-templates/js/web/jsconfig.json b/packages/create-cedar-app/templates/esm-js/web/jsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/jsconfig.json
rename to packages/create-cedar-app/templates/esm-js/web/jsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/js/web/package.json b/packages/create-cedar-app/templates/esm-js/web/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/package.json
rename to packages/create-cedar-app/templates/esm-js/web/package.json
diff --git a/packages/create-cedar-app/esm-templates/js/web/public/README.md b/packages/create-cedar-app/templates/esm-js/web/public/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/public/README.md
rename to packages/create-cedar-app/templates/esm-js/web/public/README.md
diff --git a/packages/create-cedar-app/esm-templates/js/web/public/favicon.png b/packages/create-cedar-app/templates/esm-js/web/public/favicon.png
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/public/favicon.png
rename to packages/create-cedar-app/templates/esm-js/web/public/favicon.png
diff --git a/packages/create-cedar-app/esm-templates/js/web/public/robots.txt b/packages/create-cedar-app/templates/esm-js/web/public/robots.txt
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/public/robots.txt
rename to packages/create-cedar-app/templates/esm-js/web/public/robots.txt
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/App.jsx b/packages/create-cedar-app/templates/esm-js/web/src/App.jsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/App.jsx
rename to packages/create-cedar-app/templates/esm-js/web/src/App.jsx
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx b/packages/create-cedar-app/templates/esm-js/web/src/Routes.jsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/Routes.jsx
rename to packages/create-cedar-app/templates/esm-js/web/src/Routes.jsx
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/components/.keep b/packages/create-cedar-app/templates/esm-js/web/src/components/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/components/.keep
rename to packages/create-cedar-app/templates/esm-js/web/src/components/.keep
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx b/packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/entry.client.jsx
rename to packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/index.css b/packages/create-cedar-app/templates/esm-js/web/src/index.css
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/index.css
rename to packages/create-cedar-app/templates/esm-js/web/src/index.css
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/index.html b/packages/create-cedar-app/templates/esm-js/web/src/index.html
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/index.html
rename to packages/create-cedar-app/templates/esm-js/web/src/index.html
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/layouts/.keep b/packages/create-cedar-app/templates/esm-js/web/src/layouts/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/layouts/.keep
rename to packages/create-cedar-app/templates/esm-js/web/src/layouts/.keep
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx b/packages/create-cedar-app/templates/esm-js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
rename to packages/create-cedar-app/templates/esm-js/web/src/pages/FatalErrorPage/FatalErrorPage.jsx
diff --git a/packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx b/packages/create-cedar-app/templates/esm-js/web/src/pages/NotFoundPage/NotFoundPage.jsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/src/pages/NotFoundPage/NotFoundPage.jsx
rename to packages/create-cedar-app/templates/esm-js/web/src/pages/NotFoundPage/NotFoundPage.jsx
diff --git a/packages/create-cedar-app/esm-templates/js/web/vite.config.js b/packages/create-cedar-app/templates/esm-js/web/vite.config.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/vite.config.js
rename to packages/create-cedar-app/templates/esm-js/web/vite.config.js
diff --git a/packages/create-cedar-app/esm-templates/js/web/vitest.setup.js b/packages/create-cedar-app/templates/esm-js/web/vitest.setup.js
similarity index 100%
rename from packages/create-cedar-app/esm-templates/js/web/vitest.setup.js
rename to packages/create-cedar-app/templates/esm-js/web/vitest.setup.js
diff --git a/packages/create-cedar-app/esm-templates/ts/.editorconfig b/packages/create-cedar-app/templates/esm-ts/.editorconfig
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.editorconfig
rename to packages/create-cedar-app/templates/esm-ts/.editorconfig
diff --git a/packages/create-cedar-app/esm-templates/ts/.env b/packages/create-cedar-app/templates/esm-ts/.env
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.env
rename to packages/create-cedar-app/templates/esm-ts/.env
diff --git a/packages/create-cedar-app/esm-templates/ts/.env.defaults b/packages/create-cedar-app/templates/esm-ts/.env.defaults
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.env.defaults
rename to packages/create-cedar-app/templates/esm-ts/.env.defaults
diff --git a/packages/create-cedar-app/esm-templates/ts/.env.example b/packages/create-cedar-app/templates/esm-ts/.env.example
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.env.example
rename to packages/create-cedar-app/templates/esm-ts/.env.example
diff --git a/packages/create-cedar-app/esm-templates/ts/.redwood/README.md b/packages/create-cedar-app/templates/esm-ts/.redwood/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.redwood/README.md
rename to packages/create-cedar-app/templates/esm-ts/.redwood/README.md
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json b/packages/create-cedar-app/templates/esm-ts/.vscode/extensions.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.vscode/extensions.json
rename to packages/create-cedar-app/templates/esm-ts/.vscode/extensions.json
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/launch.json b/packages/create-cedar-app/templates/esm-ts/.vscode/launch.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.vscode/launch.json
rename to packages/create-cedar-app/templates/esm-ts/.vscode/launch.json
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/settings.json b/packages/create-cedar-app/templates/esm-ts/.vscode/settings.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.vscode/settings.json
rename to packages/create-cedar-app/templates/esm-ts/.vscode/settings.json
diff --git a/packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json b/packages/create-cedar-app/templates/esm-ts/.vscode/tasks.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.vscode/tasks.json
rename to packages/create-cedar-app/templates/esm-ts/.vscode/tasks.json
diff --git a/packages/create-cedar-app/esm-templates/ts/.yarnrc.yml b/packages/create-cedar-app/templates/esm-ts/.yarnrc.yml
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/.yarnrc.yml
rename to packages/create-cedar-app/templates/esm-ts/.yarnrc.yml
diff --git a/packages/create-cedar-app/esm-templates/ts/README.md b/packages/create-cedar-app/templates/esm-ts/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/README.md
rename to packages/create-cedar-app/templates/esm-ts/README.md
diff --git a/packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma b/packages/create-cedar-app/templates/esm-ts/api/db/schema.prisma
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/db/schema.prisma
rename to packages/create-cedar-app/templates/esm-ts/api/db/schema.prisma
diff --git a/packages/create-cedar-app/esm-templates/ts/api/package.json b/packages/create-cedar-app/templates/esm-ts/api/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/package.json
rename to packages/create-cedar-app/templates/esm-ts/api/package.json
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts b/packages/create-cedar-app/templates/esm-ts/api/src/directives/requireAuth/requireAuth.test.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.test.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/directives/requireAuth/requireAuth.test.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts b/packages/create-cedar-app/templates/esm-ts/api/src/directives/requireAuth/requireAuth.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/directives/requireAuth/requireAuth.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/directives/requireAuth/requireAuth.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts b/packages/create-cedar-app/templates/esm-ts/api/src/directives/skipAuth/skipAuth.test.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.test.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/directives/skipAuth/skipAuth.test.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts b/packages/create-cedar-app/templates/esm-ts/api/src/directives/skipAuth/skipAuth.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/directives/skipAuth/skipAuth.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/directives/skipAuth/skipAuth.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts b/packages/create-cedar-app/templates/esm-ts/api/src/functions/graphql.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/functions/graphql.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/functions/graphql.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/graphql/.keep b/packages/create-cedar-app/templates/esm-ts/api/src/graphql/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/graphql/.keep
rename to packages/create-cedar-app/templates/esm-ts/api/src/graphql/.keep
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts b/packages/create-cedar-app/templates/esm-ts/api/src/lib/auth.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/lib/auth.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/lib/auth.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts b/packages/create-cedar-app/templates/esm-ts/api/src/lib/db.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/lib/db.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/lib/db.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts b/packages/create-cedar-app/templates/esm-ts/api/src/lib/logger.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/lib/logger.ts
rename to packages/create-cedar-app/templates/esm-ts/api/src/lib/logger.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/api/src/services/.keep b/packages/create-cedar-app/templates/esm-ts/api/src/services/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/src/services/.keep
rename to packages/create-cedar-app/templates/esm-ts/api/src/services/.keep
diff --git a/packages/create-cedar-app/esm-templates/ts/api/tsconfig.json b/packages/create-cedar-app/templates/esm-ts/api/tsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/tsconfig.json
rename to packages/create-cedar-app/templates/esm-ts/api/tsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts b/packages/create-cedar-app/templates/esm-ts/api/vitest.config.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/api/vitest.config.ts
rename to packages/create-cedar-app/templates/esm-ts/api/vitest.config.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/gitignore.template b/packages/create-cedar-app/templates/esm-ts/gitignore.template
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/gitignore.template
rename to packages/create-cedar-app/templates/esm-ts/gitignore.template
diff --git a/packages/create-cedar-app/esm-templates/ts/graphql.config.cjs b/packages/create-cedar-app/templates/esm-ts/graphql.config.cjs
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/graphql.config.cjs
rename to packages/create-cedar-app/templates/esm-ts/graphql.config.cjs
diff --git a/packages/create-cedar-app/esm-templates/ts/package.json b/packages/create-cedar-app/templates/esm-ts/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/package.json
rename to packages/create-cedar-app/templates/esm-ts/package.json
diff --git a/packages/create-cedar-app/esm-templates/ts/prettier.config.cjs b/packages/create-cedar-app/templates/esm-ts/prettier.config.cjs
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/prettier.config.cjs
rename to packages/create-cedar-app/templates/esm-ts/prettier.config.cjs
diff --git a/packages/create-cedar-app/esm-templates/ts/redwood.toml b/packages/create-cedar-app/templates/esm-ts/redwood.toml
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/redwood.toml
rename to packages/create-cedar-app/templates/esm-ts/redwood.toml
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/.keep b/packages/create-cedar-app/templates/esm-ts/scripts/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/scripts/.keep
rename to packages/create-cedar-app/templates/esm-ts/scripts/.keep
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/seed.ts b/packages/create-cedar-app/templates/esm-ts/scripts/seed.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/scripts/seed.ts
rename to packages/create-cedar-app/templates/esm-ts/scripts/seed.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json b/packages/create-cedar-app/templates/esm-ts/scripts/tsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/scripts/tsconfig.json
rename to packages/create-cedar-app/templates/esm-ts/scripts/tsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/ts/vitest.config.ts b/packages/create-cedar-app/templates/esm-ts/vitest.config.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/vitest.config.ts
rename to packages/create-cedar-app/templates/esm-ts/vitest.config.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/web/package.json b/packages/create-cedar-app/templates/esm-ts/web/package.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/package.json
rename to packages/create-cedar-app/templates/esm-ts/web/package.json
diff --git a/packages/create-cedar-app/esm-templates/ts/web/public/README.md b/packages/create-cedar-app/templates/esm-ts/web/public/README.md
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/public/README.md
rename to packages/create-cedar-app/templates/esm-ts/web/public/README.md
diff --git a/packages/create-cedar-app/esm-templates/ts/web/public/favicon.png b/packages/create-cedar-app/templates/esm-ts/web/public/favicon.png
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/public/favicon.png
rename to packages/create-cedar-app/templates/esm-ts/web/public/favicon.png
diff --git a/packages/create-cedar-app/esm-templates/ts/web/public/robots.txt b/packages/create-cedar-app/templates/esm-ts/web/public/robots.txt
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/public/robots.txt
rename to packages/create-cedar-app/templates/esm-ts/web/public/robots.txt
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/App.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/App.tsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/App.tsx
rename to packages/create-cedar-app/templates/esm-ts/web/src/App.tsx
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/Routes.tsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/Routes.tsx
rename to packages/create-cedar-app/templates/esm-ts/web/src/Routes.tsx
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/components/.keep b/packages/create-cedar-app/templates/esm-ts/web/src/components/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/components/.keep
rename to packages/create-cedar-app/templates/esm-ts/web/src/components/.keep
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/entry.client.tsx
rename to packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/index.css b/packages/create-cedar-app/templates/esm-ts/web/src/index.css
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/index.css
rename to packages/create-cedar-app/templates/esm-ts/web/src/index.css
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/index.html b/packages/create-cedar-app/templates/esm-ts/web/src/index.html
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/index.html
rename to packages/create-cedar-app/templates/esm-ts/web/src/index.html
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/layouts/.keep b/packages/create-cedar-app/templates/esm-ts/web/src/layouts/.keep
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/layouts/.keep
rename to packages/create-cedar-app/templates/esm-ts/web/src/layouts/.keep
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
rename to packages/create-cedar-app/templates/esm-ts/web/src/pages/FatalErrorPage/FatalErrorPage.tsx
diff --git a/packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
rename to packages/create-cedar-app/templates/esm-ts/web/src/pages/NotFoundPage/NotFoundPage.tsx
diff --git a/packages/create-cedar-app/esm-templates/ts/web/tsconfig.json b/packages/create-cedar-app/templates/esm-ts/web/tsconfig.json
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/tsconfig.json
rename to packages/create-cedar-app/templates/esm-ts/web/tsconfig.json
diff --git a/packages/create-cedar-app/esm-templates/ts/web/vite.config.ts b/packages/create-cedar-app/templates/esm-ts/web/vite.config.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/vite.config.ts
rename to packages/create-cedar-app/templates/esm-ts/web/vite.config.ts
diff --git a/packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts b/packages/create-cedar-app/templates/esm-ts/web/vitest.setup.ts
similarity index 100%
rename from packages/create-cedar-app/esm-templates/ts/web/vitest.setup.ts
rename to packages/create-cedar-app/templates/esm-ts/web/vitest.setup.ts
diff --git a/packages/create-cedar-app/vitest.config.mts b/packages/create-cedar-app/vitest.config.mts
index 377b00f059..6d9f922467 100644
--- a/packages/create-cedar-app/vitest.config.mts
+++ b/packages/create-cedar-app/vitest.config.mts
@@ -2,6 +2,6 @@ import { defineConfig, configDefaults } from 'vitest/config'
export default defineConfig({
test: {
- exclude: [...configDefaults.exclude, 'templates/**', 'esm-templates/**'],
+ exclude: [...configDefaults.exclude, 'templates/**'],
},
})