0.0.19 โข Published 7 months ago
@acets-team/ace v0.0.19

๐ง What is Ace?!
- ๐
Aceprovides Solid Fundamentals... For those that โค๏ธfine grained reactivityANDin-editor autocomplete!
๐ทโโ๏ธ Create Ace App?!
- Yes please! Just open a
bashterminal &:npx create-ace-app@latest - Wanna see what "create-ace-app" does automatically ๐ฎ?!
โ Got Features?!
Standard
- Free global hosting, ๐ธ thanks to Cloudflare! โ๏ธ
- From the server or browser, call your API endpoints as a
typesafefunction! 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 ๐ซ
- The initial
page load HTMLis created server-side for lovelySEObut then after that initial page load, enjoy smoothSingle Page Approuting & navigation! ๐งโโ๏ธ - On update... Only update... What updated ๐ช all thanks to Solid! ๐
Security
- Smaller than a photo ๐ธ b/c even when unminified
Aceis less then300 kB, requires0 dependencies& was built to be tree shaked! ๐ณ - Simplify cookies, sessions & auth, thanks to the
set(),get()&clear(), session data helpers! ๐จ - Like middleware, run
asyncfunctions before yourroute's orapi's boot! ๐
Routes & API
- Define 0 to many
layoutsforroutesto be placed within! ๐ฅ - Place your
layouts,routes&apiswherever ya love! ๐ Define,readandvalidate, path or searchparams, at api's or route's! ๐ชท- Enjoy fast initial page load times, b/c all static content is in the initial HTML! ๐
- 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! ๐
- 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
- Create new projects ๐ฉโ๐ผ and build the
autocompleting intellisense typesfor existing ones ๐๏ธ wih ourblazingly-fast cli! โก๏ธ - A super simple api, with tons of JSDOC comments for in editor docs & examples when hovering over
Acetypes, functions and components! ๐ค
Honorable Mentions
<AnimatedFor />- Animate your lovely lists, with CSS animations! ๐<Messages />- Showform save error messages, for the form as a hole AND per input, by the input! ๐ฏShimmer&LoadSpin: Show gentle loading animations with CSS! โจParamEnums: Simply define all theenumsaurl paramcan be and then validate the url (ex:'earth' | 'air' | 'fire' | 'water')parseNumber(): Also helpful w/ params, to find out is a parama number, aspecificnumber and/orbetween2 numbers to help gain url param confidence!mongoConnect()&mongoModel()- Manage mongo connection pools & enhance its standard typesafety!cuteLog()- Create strings w/ 30+ customization options to add some style to your terminal logs! ๐จholdUp()- Pause for a specific or random amount of time! โณlorem()- Generatelorem ipsumwords & 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!
}
})
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:
- First w/in your API definition:
export const GET = new API('/api/character/:element', 'apiCharacter') - & then call your API w/in the
routeorlayoutas 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'
- First w/in your API definition:
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. EveryInferhas this feature, everyInfermay be found @@ace/typesand 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>
</>
}
})
๐ How to Deploy!
Cloudflare offers free global hosting! ๐ฅน
- Create a GitHub account or Sign in
- Push to a public or private repository
- Create a Cloudlfare account or Sign in
- Navigate to
Workers & Pages - Click the
Createbutton - Click:
Import a Repository - Configure your Project
- Build Command:
npm run build - Deploy Command:
npx wrangler deploy - Save & Deploy
- Build Command:
- Copy worker env url
- Add the env url to your
./ace.config.js - Example:
envs: [ { name: 'local', url: 'http://localhost:3000' }, { name: 'prod', url: 'https://example.ace.workers.dev' }, ] - Locally at your project root (where package.json is) create
wrangler.toml - In the first line place the worker name that you gave to cloudflare:
name = "your-project-name" - On the 2nd line place today's date:
compatibility_date = "2025-01-30" - Locally navigate to
.envat your project root - For each item here, tell cloudflare about it, example:
npx wrangler secret put SESSION_CRYPT_PASSWORD - Bash:
ace build prodornpx ace build prod(npxis required when a-gis not done @npm i) - Navigate to
Workers & Pages>Your Project>Deployments - ๐ซ Push to GitHub aka Deploy! โค๏ธ
