diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index a0514a21e7..6395ca34e4 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -211,9 +211,12 @@ export default defineConfig({
},
{
text: 'Frontend',
- // collapsible: true,
- // collapsed: true,
+ collapsible: true,
items: [
+ {
+ text: 'JavaScript',
+ link: '/guides/frontend/javascript.md'
+ }
// {
// text: 'Frontend Frameworks',
// link: '/guides/frameworks.md'
diff --git a/docs/guides/basics/schemas.md b/docs/guides/basics/schemas.md
index 14079376e9..df677a1fc7 100644
--- a/docs/guides/basics/schemas.md
+++ b/docs/guides/basics/schemas.md
@@ -62,7 +62,7 @@ export const userSchema = Type.Object(
email: Type.String(),
password: Type.Optional(Type.String()),
githubId: Type.Optional(Type.Number()),
- avatar: Type.String()
+ avatar: Type.Optional(Type.String())
},
{ $id: 'User', additionalProperties: false }
)
diff --git a/docs/guides/frontend/javascript.md b/docs/guides/frontend/javascript.md
new file mode 100644
index 0000000000..affc7c972b
--- /dev/null
+++ b/docs/guides/frontend/javascript.md
@@ -0,0 +1,367 @@
+---
+outline: deep
+---
+
+# JavaScript web app
+
+As we have seen [in the quick start guide](../basics/starting.md), Feathers works great in the browser and comes with client services that allow it to easily connect to a Feathers server.
+
+In this chapter we will create a real-time chat web application with signup and login using modern plain JavaScript that connects to the API server we built in the [getting started guide](../basics/generator.md). It is mobile friendly and will work in the latest versions of Chrome, Firefox, Safari and Edge. We won't be be using a transpiler like Webpack or Babel which is also why there is no TypeScript option. The final version can be found in `public/` folder of the [feathers-chat repository](https://github.com/feathersjs/feathers-chat/tree/dove/public).
+
+![The Feathers chat application](../basics/assets/feathers-chat.png)
+
+
+
+We will not be using a frontend framework so we can focus on what Feathers is all about. Feathers is framework agnostic and can be used with any frontend framework like React, VueJS or Angular. For more information see the [frameworks section](../frameworks.md).
+
+
+
+## Set up the page
+
+First, let's update `public/index.html` to initialize everything we need for the chat frontend:
+
+```html
+
+
+
+ feathers-chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+This will load our chat CSS style, add a container div `#app` and load several libraries:
+
+- The browser version of Feathers (since we are not using a module loader like Webpack or Browserify)
+- Socket.io provided by the chat API
+- [daisyUI](https://daisyui.com/) for a collection of CSS components
+- A `client.js` for our code to live in
+
+Let’s create `public/client.js` where all the following code will live. Each of the following code samples should be added to the end of that file.
+
+## Connect to the API
+
+We’ll start with the most important thing first, the connection to our Feathers API that connects to our server using websockets and initializes the [authentication client](../basics/authentication.md):
+
+```js
+/* global io, feathers, moment */
+// Establish a Socket.io connection
+const socket = io()
+// Initialize our Feathers client application through Socket.io
+// with hooks and authentication.
+const client = feathers()
+
+client.configure(feathers.socketio(socket))
+// Use localStorage to store our login token
+client.configure(feathers.authentication())
+```
+
+## Base HTML
+
+Next, we have to define some static and dynamic HTML that we can insert into the page when we want to show the login page (which also doubles as the signup page) and the actual chat interface:
+
+```js
+// Login screen
+const loginTemplate = (error) => `
+
+
+
+ Feathers Chat
+
+
+
+
+
`
+
+// Main chat view
+const chatTemplate =
+ () => `
+
+
+
+
+
+
+
+
+
Feathers Chat
+
+ 0 User(s)
+
+
+
+
+
+
+
+
+
+
+
+
+
`
+
+// Helper to safely escape HTML
+const escapeHTML = (str) => str.replace(/&/g, '&').replace(//g, '>')
+
+const formatDate = (timestamp) =>
+ new Intl.DateTimeFormat('en-US', {
+ timeStyle: 'short',
+ dateStyle: 'medium'
+ }).format(new Date(timestamp))
+
+// Add a new user to the list
+const addUser = (user) => {
+ const userList = document.querySelector('.user-list')
+
+ if (userList) {
+ // Add the user to the list
+ userList.innerHTML += `
+
+ ${user.email}
+
+ `
+
+ // Update the number of users
+ const userCount = document.querySelectorAll('.user-list li.user').length
+
+ document.querySelector('.online-count').innerHTML = userCount
+ }
+}
+
+// Renders a message to the page
+const addMessage = (message) => {
+ // The user that sent this message (added by the populate-user hook)
+ const { user = {} } = message
+ const chat = document.querySelector('.chat')
+ // Escape HTML to prevent XSS attacks
+ const text = escapeHTML(message.text)
+
+ if (chat) {
+ chat.innerHTML += `
+
+
${user.email}
+
${formatDate(message.createdAt)}
+
${text}
+
+
`
+
+ // Always scroll to the bottom of our message list
+ chat.scrollTop = chat.scrollHeight - chat.clientHeight
+ }
+}
+```
+
+This will add the following variables and functions:
+
+- `loginTemplate` - A function that returns static HTML for the login/signup page. We can also pass an error to render an additional error message
+- `chatTemplate` - Returns the HTML for the main chat page content (once a user is logged in)
+- `addUser(user)` is a function to add a new user to the user list on the left
+- `addMessage(message)` is a function to add a new message to the list. It will also make sure that we always scroll to the bottom of the message list as messages get added
+
+## Displaying pages
+
+Next, we'll add two functions to display the login and chat page, where we'll also add a list of the 25 newest chat messages and the registered users.
+
+```js
+// Show the login page
+const showLogin = () => {
+ document.getElementById('app').innerHTML = loginTemplate()
+}
+
+// Shows the chat page
+const showChat = async () => {
+ document.getElementById('app').innerHTML = chatTemplate()
+
+ // Find the latest 25 messages. They will come with the newest first
+ const messages = await client.service('messages').find({
+ query: {
+ $sort: { createdAt: -1 },
+ $limit: 25
+ }
+ })
+
+ // We want to show the newest message last
+ messages.data.reverse().forEach(addMessage)
+
+ // Find all users
+ const users = await client.service('users').find()
+
+ // Add each user to the list
+ users.data.forEach(addUser)
+}
+```
+
+- `showLogin(error)` will either show the content of loginHTML or, if the login page is already showing, add an error message. This will happen when you try to log in with invalid credentials or sign up with a user that already exists.
+- `showChat()` does several things. First, we add the static chatHTML to the page. Then we get the latest 25 messages from the messages Feathers service (this is the same as the `/messages` endpoint of our chat API) using the Feathers query syntax. Since the list will come back with the newest message first, we need to reverse the data. Then we add each message by calling our `addMessage` function so that it looks like a chat app should — with old messages getting older as you scroll up. After that we get a list of all registered users to show them in the sidebar by calling addUser.
+
+## Login and signup
+
+Alright. Now we can show the login page (including an error message when something goes wrong) and if we are logged in, call the `showChat` we defined above. We’ve built out the UI, now we have to add the functionality to actually allow people to sign up, log in and also log out.
+
+```js
+// Retrieve email/password object from the login/signup page
+const getCredentials = () => {
+ const user = {
+ email: document.querySelector('[name="email"]').value,
+ password: document.querySelector('[name="password"]').value
+ }
+
+ return user
+}
+
+// Log in either using the given email/password or the token from storage
+const login = async (credentials) => {
+ try {
+ if (!credentials) {
+ // Try to authenticate using an existing token
+ await client.reAuthenticate()
+ } else {
+ // Otherwise log in with the `local` strategy using the credentials we got
+ await client.authenticate({
+ strategy: 'local',
+ ...credentials
+ })
+ }
+
+ // If successful, show the chat page
+ showChat()
+ } catch (error) {
+ // If we got an error, show the login page
+ showLogin(error)
+ }
+}
+```
+
+- `getCredentials()` gets us the values of the username (email) and password fields from the login/signup page to be used directly with Feathers authentication.
+- `login(credentials)` will either authenticate the credentials returned by getCredentials against our Feathers API using the local authentication strategy (e.g. username and password) or, if no credentials are given, try to use the JWT stored in localStorage. This will try and get the JWT from localStorage first where it is put automatically once you log in successfully so that we don’t have to log in every time we visit the chat. Only if that doesn’t work will it show the login page. Finally, if the login was successful it will show the chat page.
+
+## Event listeners and real-time
+
+In the last step we will add event listeners for all buttons and functionality to send new messages and make the user and message list update in real-time.
+
+```js
+const addEventListener = (selector, event, handler) => {
+ document.addEventListener(event, async (ev) => {
+ if (ev.target.closest(selector)) {
+ handler(ev)
+ }
+ })
+}
+
+// "Signup and login" button click handler
+addEventListener('#signup', 'click', async () => {
+ // For signup, create a new user and then log them in
+ const credentials = getCredentials()
+
+ // First create the user
+ await client.service('users').create(credentials)
+ // If successful log them in
+ await login(credentials)
+})
+
+// "Login" button click handler
+addEventListener('#login', 'click', async () => {
+ const user = getCredentials()
+
+ await login(user)
+})
+
+// "Logout" button click handler
+addEventListener('#logout', 'click', async () => {
+ await client.logout()
+
+ document.getElementById('app').innerHTML = loginHTML
+})
+
+// "Send" message form submission handler
+addEventListener('#send-message', 'submit', async (ev) => {
+ // This is the message text input field
+ const input = document.querySelector('[name="text"]')
+
+ ev.preventDefault()
+
+ // Create a new message and then clear the input field
+ await client.service('messages').create({
+ text: input.value
+ })
+
+ input.value = ''
+})
+
+// Listen to created events and add the new message in real-time
+client.service('messages').on('created', addMessage)
+
+// We will also see when new users get created in real-time
+client.service('users').on('created', addUser)
+
+// Call login right away so we can show the chat window
+// If the user can already be authenticated
+login()
+```
+
+- `addEventListener` is a helper function that lets us add listeners to elements that get added or removed dynamically
+- We also added click event listeners for three buttons. `#login` will get the credentials and just log in with those. Clicking `#signup` will signup and log in at the same time. It will first create a new user on our API and then log in with that same user information. Finally, `#logout` will forget the JWT and then show the login page again.
+- The `#submit` button event listener gets the message text from the input field, creates a new message on the messages service and then clears out the field.
+- Next, we added two `created` event listeners. One for `messages` which calls the `addMessage` function to add the new message to the list and one for `users` which adds the user to the list via `addUser`. This is how Feathers does real-time and everything we need to do in order to get everything to update automatically.
+- To kick our application off, we call `login()` which as mentioned above will either show the chat application right away (if we signed in before and the token hasn’t expired) or the login page.
+
+## Using the chat application
+
+That’s it. We now have a plain JavaScript real-time chat frontend with login and signup. This example demonstrates many of the basic principles of how you interact with a Feathers API. You can log in with the email (`hello@feathersjs.com`) and password (`supersecret`) from the user we registered in the [authentication chapter](../basics/authentication.md) or sign up and log in with a different email address.
+
+If you run into an issue, remember you can find the complete working example at
+
+
+
+The [feathersjs/feathers-chat-ts](https://github.com/feathersjs/feathers-chat-ts) repository
+
+
+
+
+
+The [feathersjs/feathers-chat](https://github.com/feathersjs/feathers-chat) repository
+
+
diff --git a/docs/public/crow/README.md b/docs/public/crow/README.md
deleted file mode 100644
index 700efd85a4..0000000000
--- a/docs/public/crow/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Put media resources in this folder.
diff --git a/docs/public/crow/event-filter-diagram.jpg b/docs/public/crow/event-filter-diagram.jpg
deleted file mode 100644
index 3123ad4055..0000000000
Binary files a/docs/public/crow/event-filter-diagram.jpg and /dev/null differ
diff --git a/docs/public/crow/feathers-logo-wide.png b/docs/public/crow/feathers-logo-wide.png
deleted file mode 100644
index 22ce304427..0000000000
Binary files a/docs/public/crow/feathers-logo-wide.png and /dev/null differ
diff --git a/docs/public/crow/key-image-horizontal.png b/docs/public/crow/key-image-horizontal.png
deleted file mode 100644
index 1ab70e30f0..0000000000
Binary files a/docs/public/crow/key-image-horizontal.png and /dev/null differ
diff --git a/docs/public/crow/logo-title.jpg b/docs/public/crow/logo-title.jpg
deleted file mode 100644
index 3ded9f3a86..0000000000
Binary files a/docs/public/crow/logo-title.jpg and /dev/null differ
diff --git a/docs/public/crow/profiler-log.jpg b/docs/public/crow/profiler-log.jpg
deleted file mode 100755
index 48eb571b85..0000000000
Binary files a/docs/public/crow/profiler-log.jpg and /dev/null differ
diff --git a/docs/public/crow/profiler-stats.jpg b/docs/public/crow/profiler-stats.jpg
deleted file mode 100755
index 391ddf9ed1..0000000000
Binary files a/docs/public/crow/profiler-stats.jpg and /dev/null differ
diff --git a/docs/public/crow/real-time-events-flow.jpg b/docs/public/crow/real-time-events-flow.jpg
deleted file mode 100644
index 1068828b0c..0000000000
Binary files a/docs/public/crow/real-time-events-flow.jpg and /dev/null differ
diff --git a/docs/public/crow/service-diagram-basic.jpg b/docs/public/crow/service-diagram-basic.jpg
deleted file mode 100644
index e6d38f6c55..0000000000
Binary files a/docs/public/crow/service-diagram-basic.jpg and /dev/null differ
diff --git a/docs/public/crow/services-data-store.jpg b/docs/public/crow/services-data-store.jpg
deleted file mode 100644
index 13b19226d8..0000000000
Binary files a/docs/public/crow/services-data-store.jpg and /dev/null differ
diff --git a/docs/public/crow/services-external-api.jpg b/docs/public/crow/services-external-api.jpg
deleted file mode 100644
index af5e75f33e..0000000000
Binary files a/docs/public/crow/services-external-api.jpg and /dev/null differ
diff --git a/docs/public/crow/services-external-api.png b/docs/public/crow/services-external-api.png
deleted file mode 100644
index 9eab18bc9c..0000000000
Binary files a/docs/public/crow/services-external-api.png and /dev/null differ
diff --git a/docs/public/crow/services-real-time-proxy.jpg b/docs/public/crow/services-real-time-proxy.jpg
deleted file mode 100644
index 8761efc674..0000000000
Binary files a/docs/public/crow/services-real-time-proxy.jpg and /dev/null differ
diff --git a/docs/public/crow/services-restful-actions.jpg b/docs/public/crow/services-restful-actions.jpg
deleted file mode 100644
index 8cb2cec415..0000000000
Binary files a/docs/public/crow/services-restful-actions.jpg and /dev/null differ
diff --git a/docs/public/crow/tiny-feathers-logo.png b/docs/public/crow/tiny-feathers-logo.png
deleted file mode 100644
index 9bd2947419..0000000000
Binary files a/docs/public/crow/tiny-feathers-logo.png and /dev/null differ
diff --git a/docs/public/feathers-chat.css b/docs/public/feathers-chat.css
new file mode 100644
index 0000000000..b7ee616ad0
--- /dev/null
+++ b/docs/public/feathers-chat.css
@@ -0,0 +1,484 @@
+:root {
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+body,
+#app {
+ height: 100vh;
+ width: 100vw;
+ margin: 0;
+ padding: 0;
+}
+
+/* layer: preflights */
+*,
+::before,
+::after {
+ --un-rotate: 0;
+ --un-rotate-x: 0;
+ --un-rotate-y: 0;
+ --un-rotate-z: 0;
+ --un-scale-x: 1;
+ --un-scale-y: 1;
+ --un-scale-z: 1;
+ --un-skew-x: 0;
+ --un-skew-y: 0;
+ --un-translate-x: 0;
+ --un-translate-y: 0;
+ --un-translate-z: 0;
+ --un-pan-x: ;
+ --un-pan-y: ;
+ --un-pinch-zoom: ;
+ --un-scroll-snap-strictness: proximity;
+ --un-ordinal: ;
+ --un-slashed-zero: ;
+ --un-numeric-figure: ;
+ --un-numeric-spacing: ;
+ --un-numeric-fraction: ;
+ --un-border-spacing-x: 0;
+ --un-border-spacing-y: 0;
+ --un-ring-offset-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-ring-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-shadow-inset: ;
+ --un-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-ring-inset: ;
+ --un-ring-offset-width: 0px;
+ --un-ring-offset-color: #fff;
+ --un-ring-width: 0px;
+ --un-ring-color: rgba(147, 197, 253, 0.5);
+ --un-blur: ;
+ --un-brightness: ;
+ --un-contrast: ;
+ --un-drop-shadow: ;
+ --un-grayscale: ;
+ --un-hue-rotate: ;
+ --un-invert: ;
+ --un-saturate: ;
+ --un-sepia: ;
+ --un-backdrop-blur: ;
+ --un-backdrop-brightness: ;
+ --un-backdrop-contrast: ;
+ --un-backdrop-grayscale: ;
+ --un-backdrop-hue-rotate: ;
+ --un-backdrop-invert: ;
+ --un-backdrop-opacity: ;
+ --un-backdrop-saturate: ;
+ --un-backdrop-sepia: ;
+}
+
+::backdrop {
+ --un-rotate: 0;
+ --un-rotate-x: 0;
+ --un-rotate-y: 0;
+ --un-rotate-z: 0;
+ --un-scale-x: 1;
+ --un-scale-y: 1;
+ --un-scale-z: 1;
+ --un-skew-x: 0;
+ --un-skew-y: 0;
+ --un-translate-x: 0;
+ --un-translate-y: 0;
+ --un-translate-z: 0;
+ --un-pan-x: ;
+ --un-pan-y: ;
+ --un-pinch-zoom: ;
+ --un-scroll-snap-strictness: proximity;
+ --un-ordinal: ;
+ --un-slashed-zero: ;
+ --un-numeric-figure: ;
+ --un-numeric-spacing: ;
+ --un-numeric-fraction: ;
+ --un-border-spacing-x: 0;
+ --un-border-spacing-y: 0;
+ --un-ring-offset-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-ring-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-shadow-inset: ;
+ --un-shadow: 0 0 rgba(0, 0, 0, 0);
+ --un-ring-inset: ;
+ --un-ring-offset-width: 0px;
+ --un-ring-offset-color: #fff;
+ --un-ring-width: 0px;
+ --un-ring-color: rgba(147, 197, 253, 0.5);
+ --un-blur: ;
+ --un-brightness: ;
+ --un-contrast: ;
+ --un-drop-shadow: ;
+ --un-grayscale: ;
+ --un-hue-rotate: ;
+ --un-invert: ;
+ --un-saturate: ;
+ --un-sepia: ;
+ --un-backdrop-blur: ;
+ --un-backdrop-brightness: ;
+ --un-backdrop-contrast: ;
+ --un-backdrop-grayscale: ;
+ --un-backdrop-hue-rotate: ;
+ --un-backdrop-invert: ;
+ --un-backdrop-opacity: ;
+ --un-backdrop-saturate: ;
+ --un-backdrop-sepia: ;
+}
+
+/* layer: icons */
+.i-feather-alert-triangle {
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4m0 4h.01'/%3E%3C/svg%3E");
+ mask: var(--un-icon) no-repeat;
+ mask-size: 100% 100%;
+ -webkit-mask: var(--un-icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ background-color: currentColor;
+ width: 1em;
+ height: 1em;
+}
+
+.i-feather-hash {
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 9h16M4 15h16M10 3L8 21m8-18l-2 18'/%3E%3C/svg%3E");
+ mask: var(--un-icon) no-repeat;
+ mask-size: 100% 100%;
+ -webkit-mask: var(--un-icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ background-color: currentColor;
+ width: 1em;
+ height: 1em;
+}
+
+.i-feather-log-out {
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4m7 14l5-5l-5-5m5 5H9'/%3E%3C/svg%3E");
+ mask: var(--un-icon) no-repeat;
+ mask-size: 100% 100%;
+ -webkit-mask: var(--un-icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ background-color: currentColor;
+ width: 1em;
+ height: 1em;
+}
+
+.i-feather-menu {
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
+ mask: var(--un-icon) no-repeat;
+ mask-size: 100% 100%;
+ -webkit-mask: var(--un-icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ background-color: currentColor;
+ width: 1em;
+ height: 1em;
+}
+
+.i-feather-x {
+ --un-icon: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M18 6L6 18M6 6l12 12'/%3E%3C/svg%3E");
+ mask: var(--un-icon) no-repeat;
+ mask-size: 100% 100%;
+ -webkit-mask: var(--un-icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ background-color: currentColor;
+ width: 1em;
+ height: 1em;
+}
+
+.i-logos-feathersjs {
+ background: url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='%23333' d='M128 9.102c65.665 0 118.898 53.233 118.898 118.898c0 65.665-53.233 118.898-118.898 118.898C62.335 246.898 9.102 193.665 9.102 128C9.102 62.335 62.335 9.102 128 9.102M128 0C57.421 0 0 57.421 0 128c0 70.579 57.421 128 128 128c70.579 0 128-57.421 128-128C256 57.421 198.579 0 128 0m20.83 25.524c-10.43-1.896-35.651 36.409-43.994 59.734c-.634 1.769-2.086 8.249-2.086 9.955c0 0 6.531 14.055 8.343 17.351c-3.034-1.58-9.323-13.756-9.323-13.756c-3.034 5.784-5.942 32.34-4.994 37.271c0 0 6.762 10.062 9.387 12.578c-3.603-1.201-9.671-9.355-9.671-9.355c-1.138 3.508-.916 10.807-.379 13.274c4.551 6.637 10.619 7.396 10.619 7.396s-6.637 66.181 3.413 71.111c6.258-1.327 7.775-73.956 7.775-73.956s7.585.569 9.292-1.327c3.856-2.655 12.826-30.224 12.958-34.202c0 0-10.41 1.952-15.487 3.924c3.826-3.8 16.049-6.352 16.049-6.352c3.315-3.979 10.291-31.047 10.994-39.391c.176-2.093.583-4.657.268-8.398c0 0-9.941 2.177-12.014 1.424c2.104-.237 12.263-4.14 12.263-4.14c1.801-16.213 2.358-42.091-3.413-43.141Zm-36.38 171.691c-.795 19.496-1.294 25.004-2.115 29.601c-.379.857-.758.997-1.138-.095c-3.477-15.992-3.224-136.438 36.409-191.241c-23.05 42.092-33.535 122.861-33.156 161.735Z'/%3E%3C/svg%3E") no-repeat;
+ background-size: 100% 100%;
+ background-color: transparent;
+ width: 1em;
+ height: 1em;
+}
+
+/* layer: default */
+.relative {
+ position: relative;
+}
+
+.mx-auto {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.my-5 {
+ margin-top: 1.25rem;
+ margin-bottom: 1.25rem;
+}
+
+.ml-2 {
+ margin-left: 0.5rem;
+}
+
+.mt-0 {
+ margin-top: 0rem;
+}
+
+.mt-6 {
+ margin-top: 1.5rem;
+}
+
+.block {
+ display: block;
+}
+
+.h-10 {
+ height: 2.5rem;
+}
+
+.h-2\.2 {
+ height: 0.55rem;
+}
+
+.h-32 {
+ height: 8rem;
+}
+
+.h-full {
+ height: 100%;
+}
+
+.max-w-sm {
+ max-width: 24rem;
+}
+
+.min-h-screen {
+ min-height: 100vh;
+}
+
+.w-10 {
+ width: 2.5rem;
+}
+
+.w-2\.2 {
+ width: 0.55rem;
+}
+
+.w-32 {
+ width: 8rem;
+}
+
+.w-4 {
+ width: 1rem;
+}
+
+.w-6 {
+ width: 1.5rem;
+}
+
+.w-60 {
+ width: 15rem;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.flex {
+ display: flex;
+}
+
+.flex-grow {
+ flex-grow: 1;
+}
+
+.flex-row {
+ flex-direction: row;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-start {
+ justify-content: flex-start;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.overflow-hidden {
+ overflow: hidden;
+}
+
+.overflow-y-auto {
+ overflow-y: auto;
+}
+
+.border-2 {
+ border-width: 2px;
+ border-style: solid;
+}
+
+.border-neutral {
+ border-color: hsla(var(--n));
+}
+
+.rounded {
+ border-radius: 0.25rem;
+}
+
+.bg-neutral {
+ background-color: hsla(var(--n));
+}
+
+.from-orange-500 {
+ --un-gradient-from: rgba(249, 115, 22, var(--un-from-opacity, 1));
+ --un-gradient-to: rgba(249, 115, 22, 0);
+ --un-gradient-stops: var(--un-gradient-from), var(--un-gradient-to);
+}
+
+.to-red-900 {
+ --un-gradient-to: rgba(127, 29, 29, var(--un-to-opacity, 1));
+}
+
+.bg-gradient-to-br {
+ --un-gradient-shape: to bottom right;
+ --un-gradient: var(--un-gradient-shape), var(--un-gradient-stops);
+ background-image: linear-gradient(var(--un-gradient));
+}
+
+.bg-clip-text {
+ -webkit-background-clip: text;
+ background-clip: text;
+}
+
+.p-0 {
+ padding: 0rem;
+}
+
+.p-2 {
+ padding: 0.5rem;
+}
+
+.px-3 {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+}
+
+.px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.py-8 {
+ padding-top: 2rem;
+ padding-bottom: 2rem;
+}
+
+.pb-3 {
+ padding-bottom: 0.75rem;
+}
+
+.pt-2 {
+ padding-top: 0.5rem;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-5xl {
+ font-size: 3rem;
+ line-height: 1;
+}
+
+.text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.font-bold {
+ font-weight: 700;
+}
+
+.font-light {
+ font-weight: 300;
+}
+
+.leading-4 {
+ line-height: 1rem;
+}
+
+.tracking-tight {
+ letter-spacing: -0.025em;
+}
+
+.text-error {
+ color: hsla(var(--er));
+}
+
+.text-transparent {
+ color: transparent;
+}
+
+.shadow-xl {
+ --un-shadow: var(--un-shadow-inset) 0 20px 25px -5px var(--un-shadow-color, rgba(0, 0, 0, 0.1)), var(--un-shadow-inset) 0 8px 10px -6px var(--un-shadow-color, rgba(0, 0, 0, 0.1));
+ box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);
+}
+
+.invert {
+ --un-invert: invert(1);
+ filter: var(--un-blur) var(--un-brightness) var(--un-contrast) var(--un-drop-shadow) var(--un-grayscale) var(--un-hue-rotate) var(--un-invert) var(--un-saturate) var(--un-sepia);
+}
+
+.transition-colors {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.duration-300 {
+ transition-duration: 300ms;
+}
+
+@media (min-width: 640px) {
+ .sm\:mt-1\.5 {
+ margin-top: 0.375rem;
+ }
+
+ .sm\:h-12 {
+ height: 3rem;
+ }
+
+ .sm\:w-12 {
+ width: 3rem;
+ }
+}
+
+@media (min-width: 768px) {
+ .md\:leading-5 {
+ line-height: 1.25rem;
+ }
+}
+
+@media (min-width: 1024px) {
+ .lg\:hidden {
+ display: none;
+ }
+}
diff --git a/packages/cli/src/app/templates/logger.tpl.ts b/packages/cli/src/app/templates/logger.tpl.ts
index 709808126f..c192111942 100644
--- a/packages/cli/src/app/templates/logger.tpl.ts
+++ b/packages/cli/src/app/templates/logger.tpl.ts
@@ -26,8 +26,8 @@ export const logErrorHook = async (context: HookContext, next: NextFunction) =>
logger.error(error.stack)
// Log validation errors
- if (error.errors) {
- logger.error(error.errors)
+ if (error.data) {
+ logger.error(error.data)
}
throw error