0.0.19 โ€ข Published 7 months ago

@acets-team/ace v0.0.19

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

Ace

๐Ÿง What is Ace?!

  • ๐Ÿ‘‹ Ace provides Solid Fundamentals... For those that โค๏ธ fine grained reactivity AND in-editor autocomplete!

๐Ÿ‘ทโ€โ™€๏ธ Create Ace App?!

โœ… Got Features?!

Standard

  1. Free global hosting, ๐Ÿ’ธ thanks to Cloudflare! โ˜๏ธ
  2. From the server or browser, call your API endpoints as a typesafe function!
  3. In editor, autocomplete & typesafety @:
    • Anchor Tags ๐Ÿ”—
    • Frontend & Backend Redirects ๐Ÿ”€
    • API Requests started server side during page load ๐ŸŒ
    • Async API Requests started in the browser after page load ๐Ÿ’ซ
  4. The initial page load HTML is created server-side for lovely SEO but then after that initial page load, enjoy smooth Single Page App routing & navigation! ๐Ÿงšโ€โ™€๏ธ
  5. On update... Only update... What updated ๐Ÿ’ช all thanks to Solid! ๐Ÿ™

Security

  1. Smaller than a photo ๐Ÿ“ธ b/c even when unminified Ace is less then 300 kB, requires 0 dependencies & was built to be tree shaked! ๐ŸŒณ
  2. Simplify cookies, sessions & auth, thanks to the set(), get() & clear(), session data helpers! ๐Ÿšจ
  3. Like middleware, run async functions before your route's or api's boot! ๐Ÿ”

Routes & API

  1. Define 0 to many layouts for routes to be placed within! ๐Ÿ“ฅ
  2. Place your layouts, routes & apis wherever ya love! ๐Ÿ’š
  3. Define, read and validate, path or search params, at api's or route's! ๐Ÿชท
  4. Enjoy fast initial page load times, b/c all static content is in the initial HTML! ๐Ÿ˜Š
  5. Have static content available immediately, make multiple api calls during a page reder, and as each dynamic promise resolves (ex: database data), stream that item back to the frontend! ๐ŸŽ‰
  6. So when Request processing begins on the server, so does dynamic data gathering. If dynamic data is ready before the page has been built that dynamic data will be included in the initial page load and all else will stream once that item is ready! ๐Ÿ’ซ

Getting Started

  1. Create new projects ๐Ÿ‘ฉโ€๐Ÿผ and build the autocompleting intellisense types for existing ones ๐Ÿ—๏ธ wih our blazingly-fast cli! โšก๏ธ
  2. A super simple api, with tons of JSDOC comments for in editor docs & examples when hovering over Ace types, functions and components! ๐Ÿค“

Honorable Mentions

  1. <AnimatedFor /> - Animate your lovely lists, with CSS animations! ๐ŸŒ€
  2. <Messages /> - Show form save error messages, for the form as a hole AND per input, by the input! ๐ŸŽฏ
  3. Shimmer & LoadSpin: Show gentle loading animations with CSS! โœจ
  4. ParamEnums: Simply define all the enums a url param can be and then validate the url (ex: 'earth' | 'air' | 'fire' | 'water')
  5. parseNumber(): Also helpful w/ params, to find out is a param a number, a specific number and/or between 2 numbers to help gain url param confidence!
  6. mongoConnect() & mongoModel() - Manage mongo connection pools & enhance its standard typesafety!
  7. cuteLog() - Create strings w/ 30+ customization options to add some style to your terminal logs! ๐ŸŽจ
  8. holdUp() - Pause for a specific or random amount of time! โณ
  9. lorem() - Generate lorem ipsum words & paragraphs! โœ๏ธ

๐Ÿงšโ€โ™€๏ธ Got code?!

GET! ๐Ÿ’–

import { API } from '@ace/api'

export const GET = new API('/api/aloha', 'apiAloha') // now we've got an api endpoint @ the path /api/aloha AND we can call the function apiAloha() app-wide w/ request & response typesafety!
  .resolve(async (be) => {
    return be.json({ aloha: true })
  })

Params! ๐Ÿ’œ

  • Required & optional params available @ routes & apis!
import { API } from '@ace/api'

export const GET = new API('/api/aloha/:id', 'apiAloha')
  .params<{ id: string }>() // set params type here & then this api's params are known @ .resolve() & app-wide ๐Ÿ™Œ
  .resolve(async (be) => {
    const {id} = be.getParams() // typesafe!
    return be.json({ id })
  })

Use Middleware! ๐Ÿ’™

  • Available @ routes & apis!
import { API } from '@ace/api'
import { authB4 } from '@src/lib/b4'

export const GET = new API('/api/aloha', 'apiAloha')
  .b4(authB4) // run the `authB4()` async function before this api boots!
  .resolve(async (be) => {
    return be.json({ aloha: true })
  })

Create Middleware! ๐Ÿ’š

import { go } from '@ace/go'
import type { B4 } from '@ace/types'

export const guestB4: B4 = async (ctx) => {
  if (ctx.sessionData) return go('/contracts') // go() knows about all your routes & provides autocomplete!
}

export const authB4: B4 = async (ctx) => {
  if (!ctx.sessionData) return go('/sign-in/:messageId?', {messageId: '1'})
}

POST! ๐Ÿงก

import { go } from '@ace/go'
import { API } from '@ace/api'
import { compare } from 'bcrypt'
import { guestB4 } from '@src/lib/b4'
import { SessionData } from 'ace.config'
import { M_User } from '@src/db/M_User'
import { setSessionData } from '@ace/session'
import { mongoConnect } from '@ace/mongoConnect'
import { signInSchema, SignInSchema } from '@src/schemas/SignInSchema'


export const POST = new API('/api/sign-in', 'apiSignIn')
  .b4(guestB4)
  .body<SignInSchema>() // tells .resolve() & app-wide the request body this api requires
  .resolve((be) => {
    const body = signInSchema.parse(await be.getBody()) // get, validate & parse the request body in 1 line!

    await mongoConnect() // ensures 1 mongo pool is running

    const user = await M_User.get().findOne({ email: body.email }).lean()

    if (!user) throw new Error('Please use valid credentials')

    if (!await compare(body.password, user.passwordHash)) throw new Error('Please use valid credentials')

    const sessionData: SessionData = { id: user.id }

    setSessionData(sessionData)

    return be.go('/auhenticated') // go() knows about all your routes & provides autocomplete!
  }
})

Sloths developing software in a tree

Schema ๐Ÿ’›

import { pipe, email, string, object, nonEmpty } from 'valibot'
import { ValibotSchema, type InferValibotSchema } from '@ace/valibotSchema'


export const signInSchema = new ValibotSchema( // schema's validate (be) api's above & (fe) route's below!
  object({
    email: pipe(string(), email('Please provide a valid email')),
    password: pipe(string(), nonEmpty('Please provide a password')),
  })
)

export type SignInSchema = InferValibotSchema<typeof signInSchema> // by defining runtime validations above, we get compile time types app-wide!

Layout! โค๏ธ

import './Guest.css'
import GuestNav from './GuestNav'
import { Layout } from '@ace/layout'


export default new Layout()
  .component((fe) => {
    return <>
      <div class="guest">
        <GuestNav />
        {fe.getChildren()}
      </div>
    </>
  })

Route! ๐ŸŒŸ

import { A } from '@ace/a'
import { Route } from '@ace/route'
import { Title } from '@solidjs/meta'


export default new Route('/yin') // this route uses no layouts!
  .component((fe) => {
    return <>
      <Title>Yin</Title>
      <A path="/yang">Yang</A> {/* The <A /> component knows about your routes & provides autocomplete! ๐Ÿ™Œ */}
    </>
  })

404 Route! โ˜€๏ธ

import './404.css'
import { A } from '@ace/a'
import { Title } from '@solidjs/meta'
import RootLayout from '../RootLayout'
import { Route404 } from '@ace/route404'


export default new Route404()
  .layouts([RootLayout]) // zero to many layouts available!
  .component((fe) => {
    return <>
      <Title>๐Ÿ˜… 404</Title>

      <main class="not-found">
        <div class="code">404 ๐Ÿ˜…</div>
        <div class="message">Oops! We can't find that page.</div>
        <div class="path">{fe.getLocation().pathname}</div>
        <A path="/" class="brand">๐Ÿก Go Back Home</A>
      </main>
    </>
  })

Route w/ Async Data! ๐Ÿ’ซ

  • Async data requests (seen below @ load()) run simultaneously and populate in app once resolved!
  • If this page is refreshed, data begins gathering on the server
  • If this page is navigated to via a link w/in the app (SPA navigation), then the data request starts from the browser
  • How to:
    1. First w/in your API definition: export const GET = new API('/api/character/:element', 'apiCharacter')
    2. & then call your API w/in the route or layout as many times as ya โค๏ธ:
      import '@ace/shimmer.styles.css'
      import { load } from '@ace/load'
      import { Route } from '@ace/route'
      import { Suspense } from 'solid-js'
      import { apiCharacter } from '@ace/apis'
      import type { InferLoadFn } from '@ace/types'
      import type { InferEnums } from '@ace/paramEnums'
      import type { elementEnums } from '@src/lib/vars'
export default new Route('/smooth')
  .component(() => {
    const air = load(() => apiCharacter({params: {element: 'air'}}), 'air')
    const fire = load(() => apiCharacter({params: {element: 'fire'}}), 'fire')
    const earth = load(() => apiCharacter({params: {element: 'earth'}}), 'earth')
    const water = load(() => apiCharacter({params: {element: 'water'}}), 'water')

    return <Characters res={{ air, fire, earth, water }} />
  })

function Characters({ res }: { res: Record<InferEnums<typeof elementEnums>, InferLoadFn<'apiCharacter'>> }) {
  return <>
    <div class="characters">
      <Character element={res.fire} />
      <Character element={res.water} />
      <Character element={res.earth} />
      <Character element={res.air} />
    </div>
  </>
}


function Character({ element }: { element: InferLoadFn<'apiCharacter'> }) {
  return <>
    <div class="character">
      <Suspense fallback={<div class="ace-shimmer"></div>}>
        {element()?.error?.message || element()?.data?.character}
      </Suspense>
    </div>
  </>
}
```

Infer! ๐Ÿงšโ€โ™€๏ธ

  • In the example above we use InferLoadFn
  • When using an Infer, example: InferLoadFn<''>, place your insertion point in the string, press control + space & get autocomplete. Every Infer has this feature, every Infer may be found @ @ace/types and the most frequently used are:
    • โœ… InferLoadFn
      • Autocomplete: All API Functions
      • Type: API Function Response
      • Example: InferLoadFn<'apiCharacter'>
    • โœ… InferResponseGET
      • Autocomplete: Path to each api GET
      • Type: API Response Body
      • Example: InferResponseGET<'/api/example'>
    • โœ… InferParamsGET
      • Autocomplete: Path to each api GET
      • Type: API Params
      • Example: InferParamsGET<'/api/example/:id'>
    • โœ… InferBodyPOST
      • Autocomplete: Path to each api POST
      • Type: API Request Body
      • Example: InferBodyPOST<'/api/example'>
    • โœ… InferParamsRoute
      • Autocomplete: Path to each route
      • Type: Route Params
      • Example: InferParamsRoute<'/example/:id'>

Form! โœจ

import { clear } from '@ace/clear'
import { Route } from '@ace/route'
import { Submit } from '@ace/submit'
import { apiSignUp } from '@ace/apis'
import { Title } from '@solidjs/meta'
import { guestB4 } from '@src/lib/b4'
import RootLayout from '../RootLayout'
import GuestLayout from './Guest.Layout'
import { Messages } from '@ace/messages'
import { createOnSubmit } from '@ace/createOnSubmit'
import { signUpSchema } from '@src/schemas/SignUpSchema'


export default new Route('/sign-up/:sourceId?')
  .b4(guestB4) // run this asyc fn b4 route render
  .layouts([RootLayout, GuestLayout]) // Root wraps Guest, Guest wraps this Route!
  .component((fe) => {
    const onSubmit = createOnSubmit(async (fd) => { // createOnSubmit() places this async fn() into a try/catch for us & on fe or be catch, <Messages /> get populated below!
      const body = signUpSchema.parse({ // get parse & validate request body
        email: fd('email'), // fd() is a form data helper
        password: fd('password')
      }) 

      await apiSignUp({ body, bitKey: 'signUp' }) // a bit is a boolean signal ๐Ÿ’ƒ & the body has autocomplete!
    })

    return <>
      <Title>Sign Up</Title>

      <form onSubmit={onSubmit}>
        <input placeholder="Email" name="email" type="email" use:clear />
        <Messages name="email" /> {/* shows messages, from signUpSchema.parse() and/or fe.POST(), for just the email input! ๐Ÿš€ */}

        <input placeholder="Password" name="password" type="password" use:clear /> {/* the use:clear directive clears password <Messages /> on first interaction w/ this input! */}
        <Messages name="password" />

        <div class="footer">
          <Submit label="Sign Up" bitKey="signUp" /> {/* Uses fe.bits.isOn('signUp') to show a loading indicator! ๐Ÿ‹๏ธโ€โ™‚๏ธ */}
        </div>
      </form>
    </>
  }
})

Squirrel Engineer

๐Ÿš€ How to Deploy!

Cloudflare offers free global hosting! ๐Ÿฅน

  1. Create a GitHub account or Sign in
  2. Push to a public or private repository
  3. Create a Cloudlfare account or Sign in
  4. Navigate to Workers & Pages
  5. Click the Create button
  6. Click: Import a Repository
  7. Configure your Project
    • Build Command: npm run build
    • Deploy Command: npx wrangler deploy
    • Save & Deploy
  8. Copy worker env url
  9. Add the env url to your ./ace.config.js
  10. Example:
    envs: [
      { name: 'local', url: 'http://localhost:3000' },
      { name: 'prod', url: 'https://example.ace.workers.dev' },
    ]
  11. Locally at your project root (where package.json is) create wrangler.toml
  12. In the first line place the worker name that you gave to cloudflare: name = "your-project-name"
  13. On the 2nd line place today's date: compatibility_date = "2025-01-30"
  14. Locally navigate to .env at your project root
  15. For each item here, tell cloudflare about it, example: npx wrangler secret put SESSION_CRYPT_PASSWORD
  16. Bash: ace build prod or npx ace build prod (npx is required when a -g is not done @ npm i)
  17. Navigate to Workers & Pages > Your Project > Deployments
  18. ๐Ÿ’ซ Push to GitHub aka Deploy! โค๏ธ

Bunnies writing code

๐Ÿ’– Next!

0.0.19

7 months ago

0.0.18

7 months ago

0.0.17

7 months ago

0.0.16

7 months ago

0.0.15

7 months ago

0.0.14

7 months ago

0.0.13

7 months ago

0.0.12

7 months ago

0.0.11

8 months ago

0.0.10

8 months ago

0.0.9

8 months ago

0.0.8

8 months ago

0.0.7

8 months ago

0.0.6

8 months ago

0.0.5

8 months ago

0.0.4

8 months ago

0.0.3

8 months ago