@acets-team/ace
v0.9.5
Published
Our Mission: π Unite industry leaders, to provide optimal web fundamentals, in a performant, typesafe and beautifully documented library! π
Downloads
2,128
Maintainers
Readme

Table of Contents
- What is Ace?
- Ace Mission Statement
- Create Ace App
- Save state to indexdb
- Create API Route
- Create Middleware
- Path and Search Params
- Valibot Helpers
- Create a Layout
- Create a Route
- Call APIs
- π VS Code Extension
- Echo
- Breakpoints
- Scope
- Create a 404 Route
- Create a Typesafe Anchor
- Typesafe Redirects
- Add Offline Support
- Network Status Hook
- π©βπ» Create Desktop Application
- Enums
- Markdown
- Code Highlight Markdown
- Bits
- Modal Demo
- π οΈ Bind DOM Elements
- Form Demo
- Magic Link Demo
- Create Password Hash
- π’ Live Data without Browser Refresh
- Open Graph Demo
- SVG Demo
- Add Custom Fonts
- Loading Spinner Component
- Show Toast Notifications
- Tabs Component
- Radio Cards Component
- Slidshow Component
- Ace Config
- Ace Plugins
- π¨ When to restart dev?
- IF developing multiple Ace projects simultaneously
- Environment Variables
- Environment Information
- Origin Information
- Add Tailwind
- Turso Demo
- AgGrid Demo
- Chart.js Demo
- Send Brevo Emails
- π Deploy
- Add a custom domain
- Resolve www DNS
- View Production Logs
- VS Code Helpful Info
- Error Dictionary
What is Ace?
- Ace is a set of functions, classes, and types (fundamentals) to aid web developers. Weβve grouped these fundamentals into plugins. To use a plugins fundamentals, set that plugin to true in your
ace.config.js! π - Ace plugins:
- Solid (optimal DOM updates)
- Drizzle (typesafe db updates)
- Turso (Fast SQL DB)
- Cloudflare (Region: Earth)
- AgGrid (Scrollable, filterable & sortable tables)
- Charts.js (Evergreen charting library)
- Valibot (Small bundle Zod)
- Brevo (300 emails a day for free)
- Markdown-It (Markdown to HTML)
- Highlight.js (Highlight code in Markdown)
Ace Mission Statement
π Unite industry leaders, to provide optimal web fundamentals, in a performant, typesafe and beautifully documented library! π
Create Ace App!
- Bash:
npx create-ace-app@latest - π¨ When opening
Create Ace Applocally for the first time after annpm run dev, it will take 10-15 seconds to load π‘ b/c Vite is altering code to optimizeHMR(so subsequent loads are instant π€) BUT this slow initial load is no factor in production. To prove this, here's Create Ace App In Production! π Deployed to Cloudflare Workers viagit push, deploy directions here!
Save state to indexdb
- β
With Ace each piece of state is called an
Atom - β
Atoms are saved @
memory,session storage,local storage(5mb + sync) orindexdb(100's of mb's + async) - β
An Atom's
isprop helps us know how to serialize to and deserialze from the save location - β
Setting custom
onSerialize/onDeserialze@new Atom()is avaialble!
- Define atoms:
src/store/atoms.ts// Create Ace App's atoms import { Atom } from '@ace/atom' import type { ApiName2Data, ChartJsMap } from '@ace/types' import type { FinanceSummary, Transaction, ChatMessage } from '@src/lib/types' export const atoms = { count: new Atom({ save: 'idb', is: 'number', init: 0 }), chatMessage: new Atom({ save: 'idb', is: 'string', init: '' }), chatMessages: new Atom<ChatMessage[]>({ save: 'idb', is: 'json', init: [] }), transactions: new Atom<Transaction[]>({ save: 'idb', is: 'json', init: [] }), financeCategories: new Atom<ChartJsMap[]>({ save: 'idb', is: 'json', init: [] }), financeSummary: new Atom<undefined | FinanceSummary>({ save: 'idb', is: 'json' }), fortunes: new Atom<ApiName2Data<'apiGetFortune'>[]>({ save: 'idb', is: 'json', init: [] }), } - Create a Store:
src/store/store.ts// To work w/ atoms we put them into a store via createStore() and then get back a useStore() function that can be used in component, route or layout // Multiple stores is simple but probably not necessary import { atoms } from './atoms' import { createMemo } from 'solid-js' import { Atoms2Store } from '@ace/types' import { createStore } from '@ace/createStore' export const { useStore, StoreProvider } = createStore({ atoms }) - Wrap
<App/>with<StoreProvider/>@src/app.tsx// createApp() will generate all our <Route />'s for us and wrap them w/ the providers in the provided array :) import './app.css' import '@ace/tabs.styles.css' import '@ace/toast.styles.css' import '@ace/pulse.styles.css' import '@ace/loading.styles.css' import '@ace/tooltip.styles.css' import { createApp } from '@ace/createApp' import { StoreProvider } from '@src/store/store' export default createApp([StoreProvider]) useStore()has lots of lovely goodies including:store- Solid's store is the accessor to all our store values
- Each
Atomis added into the store and is accessible via it'skey. So based on the example atoms above, we can access:store.countstore.newsletterForm.name
setStore()- Solid's setStore() helps us to assign store value, examples:
setStore('count', 1)setStore('count', c => c + 1)setStore('newsletterForm', 'name', 'Chris')setStore('newsletterForm', {name: 'Chris', email: '[email protected]'})
- Solid's setStore() helps us to assign store value, examples:
save()- Accepts a store key, and will
saveto the Atom's save location, example:save('count')save('newsletterForm')
- Accepts a store key, and will
set()- Calls Solid setStore() and then calls
save(), examples:set('count', 1)set('count', c => c + 1)set('newsletterForm', 'name', 'Chris')set('newsletterForm', {name: 'Chris', email: '[email protected]'})
- Calls Solid setStore() and then calls
copy()- Calls Solid's produce() which lets us mutate a copy/draft of the data inside a single callback and then calls
save(), examples:copy('user', u => { u.settings.theme = 'dark' u.todos.push('Improve Ace!') }) - π¨ The entire root object
usergets a new reference - All nested objects/arrays inside it also get new references
- This means every reactive computation that depends on any part of
userwill re-run - To avoid this please use
sync()
- Calls Solid's produce() which lets us mutate a copy/draft of the data inside a single callback and then calls
sync()- Calls Solid's reconcile() which efficiently diffs and merges entire objects or arrays and then calls
save(), examples:// array example: store.users = [ // current users { _id: '1', name: 'Chris', active: true }, { _id: '2', name: 'Alex', active: false }, ] const newUsers = [ // updated api users { _id: '1', name: 'Chris', active: false }, // changed { _id: '2', name: 'Alex', active: false }, // identical ] // Only Chris's active property triggers reactive updates // key is optional, if undefined the key is 'id' sync('users', newUsers, { key: '_id' }) // object example: store.user = { _id: '123', profile: { name: 'Chris', stats: { posts: 9, followers: 27 }, }, } const newUser = { profile: { stats: { followers: 30 } }, } sync('user', newUser)
- Calls Solid's reconcile() which efficiently diffs and merges entire objects or arrays and then calls
Create API Route
- Example:
src/api/apiUpdateEmail.ts// to the API constructor, the first arg is the path param and the 2nd is the api function name which can be called on the FE or BE import { API } from '@ace/api' import { eq } from 'drizzle-orm' import { db, users } from '@src/lib/db' import { sessionB4 } from '@src/auth/authB4' import { updateEmailParser } from '@src/parsers/updateEmailParser' export const POST = new API('/api/update-email', 'apiUpdateEmail') .b4([sessionB4]) .body(updateEmailParser) .resolve(async (scope) => { await db .update(users) .set({ email: scope.body.email }) // typesafe b/c updateEmailParser .where(eq(users.id, scope.event.locals.session.userId)) // typesafe b/c sessionB4 return scope.success('Updated!') }) - Create middleware functions:
src/auth/authB4.ts// Ace middleware is different then express middleware and for that reason we use a different name we call them b4 (before) functions b/c they run before your API resolve function // If a b4 returns anything that response is given as the Response // To persist data from one b4 to the next or from a b4 to the resolve, place data into `event.locals` and update the functions generic type π¨ By setting the generic type this lets downstream b4's or resolves know the type of your persisted data, so then in the resolve for example we'd have typesafety for `scope.event.locals.session` as seen in the api example above import type { B4 } from '@ace/types' import { getSession } from './getSession' import type { Session } from '@src/lib/types' export const sessionB4: B4<{ session: Session }> = async (scope) => { scope.event.locals.session = await getSession(scope) } export const adminB4: B4<{ session: Session }> = async (scope) => { if (!scope.event.locals.session.isAdmin) throw new Error('Unauthorized') // β } - Create a parser:
src/parsers/updateEmailParser.tsimport { object } from 'valibot' import { vParse } from '@ace/vParse' import { vEmail } from '@ace/vEmail' export const updateEmailParser = vParse( object({ email: vEmail('Please provide a valid email'), }) )
Path and Search Params
π¨ Important
.pathParams()&.searchParams()@new Route()&new API()work the same way!- IF params are valid THEN
scope.pathParamsand/orscope.searchParamswill be set w/ their typesafe parsed values! - And when working w/
new API()you may also pass a parser to.body()which if valid will be available @scope.body
β Examples:
- Optional path params:
import { vString } from '@ace/vString' import { object, optional } from 'valibot' export default new Route('/unsubscribe/:email?') .pathParams(vParse(object({ email: optional(vString()) }))) - Required path params:
export default new Route('/magic-link/:token') .pathParams(vParse(object({ token: vString('Please provide a token') }))) - Optional search params:
export const GET = new API('/api/test', 'apiTest') .searchParams(vParse(object({ amount: optional(vNum()) }))) .resolve(async (scope) => { return scope.success(scope.searchParams) }) - Required search & body params:
export const POST = new API('/api/test', 'apiTest') .body(vParse(object({ when: vDate() }))) .searchParams(vParse(object({ allGood: vBool() }))) .resolve(async ({ success, body, searchParams }) => { return success({ body, searchParams }) }) - All valibot helpers that can be used w/in
vParse(object(())
Parser
- In
AceaParseris afunctionthatvalidates& also optionallyparsesdata - This function accepts an
input, willthrowif invalid, andreturndata if valid that's potentially but not required to be parsed (your call) (allvParsehelpers below validate + parse) - Within a
Parserfunction any prefered schema library may be used π - We reccomend Valibot!
- To create a valibot parser use
vParse+ one/many of these helpers:{ token: vString('Please provide a token') }{ modal: vBool('Please provide a modal param') }{ email: vEmail('Please provide a valid email') }{ value: vNum('Please provide a value as a number') }{ voicePart: vEnums(new Enums(['Bass', 'Alto', 'Soprano'])) }{ passportIssuedDate: vDate({error: 'Please provide date passport issued'}) }
- Any of the helpers can be wrapped in valibot's
optional()btw! - Example:
vParse(object({ amount: optional(vNum()), allGood: vBool() })) vBool()- Parses
1totrue - Parses
0tofalse - Parses
trueof any case totrue - Parses
falseof any case tofalse
- Parses
vDate()- Can receive a data as a
Date|iso stringorms number - Parses based on requested
toprop @param props.to- Optional, defaults toiso, what type of date are we parsing to, options are'date' | 'ms' | 'sec' | 'iso'props.includeIsoTime- Optional, defaults totrue, when true =>2025-08-01T00:50:28.809Zwhen false =>2025-08-01@param props.error- The error to return when the input is invalid
- Can receive a data as a
- π¨
Body Parsersor parsers that validate request bodies are typically in their own file, b/c they are used on theFE& on theBE. If this parser was exported from the API file above and we imported it into aFEcomponent, then there is a risk thatAPI contentcould be in ourFEbuild. So to avoid this, when we have code that works in both environments, it'sbest practiceto put the export in it'sown file(or a file that is as a whole good in both environments likesrc/lib/vars.ts), and ensure the imports into this file areoptimalin both environments, example:// src/parsers/updateEmailParser.ts import { object } from 'valibot' import { vParse } from '@ace/vParse' import { vEmail } from '@ace/vEmail' export const updateEmailParser = vParse( object({ email: vEmail('Please provide a valid email'), }) ) // src/api/apiUpdateEmail.ts import { API } from '@ace/api' import { eq } from 'drizzle-orm' import { db, users } from '@src/lib/db' import { sessionB4 } from '@src/auth/authB4' import { updateEmailParser } from '@src/parsers/updateEmailParser' export const POST = new API('/api/update-email', 'apiUpdateEmail') .b4([sessionB4]) .body(updateEmailParser) .resolve(async (scope) => { await db .update(users) .set({ email: scope.body.email }) .where(eq(users.id, scope.event.locals.session.userId)) return scope.success() }) // FE component const {store} = useStore() const onSubmit = createOnSubmit(({ event }) => { apiUpdateEmail({ body: kParse(updateEmailParser, { email: store.newsletterForm.email }), onSuccess() { event.currentTarget.reset() showToast({ type: 'success', value: 'Updated!' }) } }) }) kParse()- The
kstands forkeys - We pass to
kParse()theinputobject (data to give to the parser) & the parser - At compile-time
kParse()reads the parser and gives us autocomplete hints for the keys this parser requires - So w/ the example above, our IDE will error till
emailis akeyin theinputobject - At run-time the parser will validate the
inputobject completely - This helps us build our
inputobject correctly
- The
createOnSubmit()- Parsers for
pathand/orsearchparams can be in their own file but are typically inline b/c they are only needed in that one place (most often but sometimes shared so then a file like above is ideal), example:export const POST = new API('/api/test', 'apiTest') .searchParams(vParse(object({ ready: vBool() }))) .resolve(({ success, searchParams }) => { return success({ searchParams }) })
Create a Layout
- Example:
src/app/RootLayout.tsx// one to many layouts may surround a route // <ServiceWorker /> is fantastic if you'd love your site to function (simply or fully) w/o internet! import { Nav } from '@src/Nav/Nav' import { Layout } from '@ace/layout' import { ServiceWorker } from '@ace/serviceWorker' export default new Layout() .component(({children}) => { return <> <Nav /> {children} <ServiceWorker /> </> })
Create a Route
- Example:
src/app/Deposits/Deposits.tsx// π¨ Vite prefers css paths to be relative so don't use @src there please import './Deposits.css' import { Show } from 'solid-js' import { Route } from '@ace/route' import { Title } from '@solidjs/meta' import RootLayout from '../RootLayout' import { Users } from '@src/Users/Users' import { getUsersSources } from '@src/lib/vars' import { loadSession } from '@src/auth/loadSession' import { AuthLoading } from '@src/auth/AuthLoading' import { isEmployee, useStore } from '@src/store/store' import { AuthHeroTitle } from '@src/auth/AuthHeroTitle' import type { ScopeComponent } from '@ace/scopeComponent' export default new Route('/deposits') .layouts([RootLayout]) .component((scope) => { loadSession() return <> <Title>β€οΈ Deposits</Title> <main class="deposit"> <AuthHeroTitle /> <div class="main-content"> <Show when={scope.bits.get('apiGetSession') === false} fallback={<AuthLoading />}> <Loaded scope={scope} /> </Show> </div> </main> </> }) function Loaded({ scope }: { scope: ScopeComponent }) { const {store} = useStore() return <> <Show when={isEmployee(store)}> <div class="h-title">β€οΈ Made Deposit</div> <Users scope={scope} source={getUsersSources.keys.deposits}></Users> </Show> </> } - As seen above w/
isEmployee(store), when computed properties are used in multiple components:src/store/store.ts:import { atoms } from './atoms' import { createMemo } from 'solid-js' import { Atoms2Store } from '@ace/types' import { partners } from '@src/lib/vars' import { createStore } from '@ace/createStore' export const { useStore, StoreProvider } = createStore({ atoms }) // computed properties that are accessed in multiple modules export const isEmployee = (store: Atoms2Store<typeof atoms>) => createMemo(() => store.apiGetSession?.isAdmin || store.apiGetSession?.partner === partners.keys.globus) export const isAdmin = (store: Atoms2Store<typeof atoms>) => createMemo(() => store.apiGetSession?.isAdmin)
Call APIs
- With
Ace, we call APIs via API functions! πfunction UpdateEmail() { const {store, refBind} = useStore() const onSubmit = createOnSubmit(({ event }) => { apiUpdateEmail({ // API Function! β€οΈ body: kParse(updateEmailParser, { email: store.newsletterForm.email }), onSuccess() { event.currentTarget.reset() showToast({ type: 'success', value: 'Updated!' }) // from @ace/toast } }) }) return <> <form ref={refFormReset()} onSubmit={onSubmit} class="update-email"> <input ref={refBind('newsletterForm', 'email')} name="email" type="email" placeholder="Please enter email" /> <Messages name="email" /> <Submit label="Update" bitKey="apiUpdateEmail" $button={{ class: 'brand' }} /> </form> </> } onSuccess()- On
FE, IF no errors ANDonSuccessprovided THENonSuccesswill be called w/response.data, which is set on theBEviareturn scope.success({ example: true })
- On
onResponse()- On
FE, IF no errors ANDonResponseprovided THENonResponsewill be called w/Response, which is set on theBEviareturn new Response('example')
- On
onError()- On
FE, IFresponse.erroris truthy THENonError()ORdefaultOnError()will be called w/response.error, which is set on theBEviareturn scope.error('π')ORthrow new Error('β') defaultOnError()=>showErrorToast(error.message)
- On
queryType- If you specify a
queryTypethen we'll use Solid'squery()function.query()accomplishes the following:- Deduping on the server for the lifetime of the request
- Provides a
back/forward cachefor browser navigation for up to 5 minutes. Any user based navigation or link click bypasses this cache
- π¨ set the
queryTypetostreamwhen you'd love this api call to happen while the component is rendering & this request does NOT set cookies. On refresh the request will start on theBEand on SPA navigation (on anchor click) the request will start on theFEimport './Home.css' import { Route } from '@ace/route' import { useStore } from '@src/store/store' import { Title, Meta } from '@solidjs/meta' import { MarkdownItStatic } from '@ace/markdownItStatic' import { apiGetFinances, apiGetCashFlow, apiGetTransactions } from '@ace/apis' export default new Route('/') .layouts([RootLayout]) .component(() => { const {sync} = useStore() apiGetTransactions({ // api's load simultaneously btw β€οΈ queryType: 'stream', onSuccess: (d) => sync('transactions', d) }) apiGetFinances({ // on refresh => requests start on the BE π€ queryType: 'stream', onSuccess(d) { sync('financeSummary', d.summary) sync('financeCategories', d.categories) } }) return <> <Title>π‘ Home Β· Create Ace App</Title> <Meta property="og:title" content="π‘ Home Β· Create Ace App" /> <Meta property="og:type" content="website" /> <Meta property="og:url" content={buildOrigin} /> <Meta property="og:image" content={buildOrigin + '/og/home.webp'} /> <main class="home"> <Welcome /> <section class="summaries"> <Summary key="balance" label="πΈ Total Balance" /> <Summary key="monthlyExpenses" label="π Monthly Expenses" /> <Summary key="monthlyIncome" label="π Monthly Income" /> </section> <section class="vizs"> <Categories /> <Transactions/> </section> <MarkdownItStatic content={mdAppInfo} /> <Nav showRefresh={true} /> </main> </> }) - π¨ set the
queryTypetodirectwhen this api call does not happen while the component is rendering but does happen after, like based on some user interaction like an onClickimport { Show } from 'solid-js' import { Submit } from '@ace/submit' import { apiSignOut } from '@ace/apis' import { scope } from '@ace/scopeComponent' import { useStore } from '@src/store/store' import { goSignIn } from '@src/auth/goSignIn' import { createOnSubmit } from '@ace/createOnSubmit' export function SignOut() { const {set, store} = useStore() const signOut = createOnSubmit(() => { apiSignOut({ // API Function! β€οΈ queryType: 'direct', onSuccess () { set('apiGetSession', undefined) goSignIn(scope) } }) }) return <> <Show when={store.apiGetSession?.userId}> <form onSubmit={signOut}> <Submit label="Sign Out" bitKey="apiSignOut" $button={{class: 'brand'}} /> </form> </Show> </> } - π¨ set the
queryTypetomaySetCookieswhen you'd love this api call to happen while the component is rendering & this request DOES set cookies.maySetCookiesensures that this request will always start on theFE. Explanation: The way a server tells a browser about cookies is w/ theSet-Cookieheader. We may not update HTTP headers after aResponseis given to theFE, and during streaming the response is already w/ theFE.streamis the most performant option, so to avoid this option as much as possible we recommend redirecting to api's that set cookiesimport { apiGetSession } from '@ace/apis' import { useStore } from '@src/store/store' export function loadSession() { const {set} = useStore() apiGetSession({ // API Function! β€οΈ queryType: 'maySetCookies', onSuccess: (d) => set('apiGetSession', d), onError: () => set('apiGetSession', undefined), }) }
- If you specify a
- π¨ See VS Code Extension to see how to add links to
Ace API'sright above API Function calls! - See Echo to see how updating API data in Ace works
queryKey:- An optional prop that can be passed to api functions
- The unique identifier
Solid's query()uses for all it's magic like deduplication - IF
queryTypeis set ANDqueryKeyis undefined THEN thequeryKeyis set as the api function name (ex:apiGetUsers) - Typically being undefined is ideal unless there is a query or search param that is sent in this request and we wanna
dedupbased on thatid(ex:['apiGetUser', id]) - So a
queryKeycan beundefined(typical), can be a string (uncommon), or an array (sometimes super helpful)
VS Code Extension
- Provides links to
Ace API'sright above API Function calls! π
- In VS Code or any fork like VsCodium:
- Extensions Search:
ace-vs-code - The name of the package is
Ace for VS Codeand the author isacets-team
- Extensions Search:
- & please feel free click here to see additional vs code helpful info!
Echo
- How does this
Echoidea work?- An
APIendpoint updates some data - The
APIgathers the updated data consistently (same function every time) - The
APIechos the updated data to theFEw/in theResponse - & if using
Ace Live Server, theAPIechos the updated data to all subscribers
- An
- π€ Kitchen Sink Example:
src/lib/vars.tsexport const streams = new Enums(['amsterdamRegistration']) // the streams our Ace Live Server supportssrc/lib/types.d.tsimport type { ApiName2Data } from '@ace/types' export type LiveAmsterdamRegistrationData = { // subscribers will receive a toast message & the updated registration message: string, registration: ApiName2Data<'apiGetAmsterdamRegistration'> }src/db/dbGetAmsterdamRegistration.ts// our consistent (same function every time) way to get complex data (complex b/c multiple joins) import { eq, type SQL } from 'drizzle-orm' import { amsterdamRegistrations, db, roommateOptions, users, voiceParts } from "./db"; export async function dbGetAmsterdamRegistration(where: SQL) { const [registration] = await db .select({ id: amsterdamRegistrations.id, email: users.email, phone: amsterdamRegistrations.phone, gender: amsterdamRegistrations.gender, voicePart: voiceParts.name, emergencyContact: amsterdamRegistrations.emergencyContact, physicalLimitations: amsterdamRegistrations.physicalLimitations, dietaryLimitations: amsterdamRegistrations.dietaryLimitations, roommateOption: roommateOptions.name, roommateName: amsterdamRegistrations.roommateName, singleAgree: amsterdamRegistrations.roommateSingleAgree, nameOnPassport: amsterdamRegistrations.name, passportNumber: amsterdamRegistrations.passportNumber, passportIssuedDate: amsterdamRegistrations.passportIssuedDate, passportExpiredDate: amsterdamRegistrations.passportExpiredDate, passportAuthority: amsterdamRegistrations.passportAuthority, nationality: amsterdamRegistrations.nationality, }) .from(amsterdamRegistrations) .leftJoin(voiceParts, eq(amsterdamRegistrations.voicePartId, voiceParts.id)) .leftJoin(roommateOptions, eq(amsterdamRegistrations.roommateOptionId, roommateOptions.id)) .leftJoin(users, eq(users.id, amsterdamRegistrations.userId)) .where(where) .limit(1) return registration }src/api/apiGetAmsterdamRegistration.tsimport { API } from '@ace/api' import { eq } from 'drizzle-orm' import { vNum } from '@ace/vNum' import { vParse } from '@ace/vParse' import { object, optional } from 'valibot' import { sessionB4 } from '@src/auth/authB4' import { amsterdamRegistrations } from '@src/db/db' import { dbGetAmsterdamRegistration } from '@src/db/dbGetAmsterdamRegistration' export const GET = new API('/api/get-amsterdam-registration/:registrationId?', 'apiGetAmsterdamRegistration') .b4([sessionB4]) .pathParams(vParse(object({ registrationId: optional(vNum()) }))) .resolve(async (scope) => { const where = scope.pathParams.registrationId // IF pathParam defined ? eq(amsterdamRegistrations.id, scope.pathParams.registrationId) // get registration by pathParams.registrationId : eq(amsterdamRegistrations.userId, scope.event.locals.session.userId) // get registration by current session user id const registration = await dbGetAmsterdamRegistration(where) return scope.success(registration) })src/api/apiUpsertAmsterdamRegistration.tsimport { API } from '@ace/api' import { streams } from '@src/lib/vars' import { sessionB4 } from '@src/auth/authB4' import { eq, type InferInsertModel } from 'drizzle-orm' import { db, amsterdamRegistrations } from '@src/db/db' import type { LiveAmsterdamRegistrationData } from '@src/lib/types' import { dbGetAmsterdamRegistration } from '@src/db/dbGetAmsterdamRegistration' import { upsertAmsterdamRegistrationParser } from '@src/parsers/upsertAmsterdamRegistrationParser' export const POST = new API('/api/upsert-amsterdam-registration', 'apiUpsertAmsterdamRegistration') .b4([sessionB4]) .body(upsertAmsterdamRegistrationParser) .resolve(async (scope) => { if (!process.env.LIVE_SECRET) throw new Error('!process.env.LIVE_SECRET') const user = await db.query.users.findFirst({ where: (u, { eq }) => eq(u.id, scope.event.locals.session.userId) }) if (!user) throw new Error('Please sign in') const voicePart = await db.query.voiceParts.findFirst({ where: (v, { eq }) => eq(v.name, scope.body.voicePart) }) if (!voicePart) throw new Error('Please include a valid voice part') const roommateOption = await db.query.roommateOptions.findFirst({ where: (r, { eq }) => eq(r.name, scope.body.roommateOption) }) if (!roommateOption) throw new Error('Please include a valid roommate option') const existingRegistration = await db.query.amsterdamRegistrations.findFirst({ where: (row, { eq }) => eq(row.userId, user.id) }) const upsert: InferInsertModel<typeof amsterdamRegistrations> = { // β€οΈ define 1 shape for insert AND update userId: user.id, phone: scope.body.phone, gender: scope.body.gender, voicePartId: voicePart.id, emergencyContact: scope.body.emergencyContact, physicalLimitations: scope.body.physicalLimitations || null, dietaryLimitations: scope.body.dietaryLimitations || null, roommateOptionId: roommateOption.id, roommateName: scope.body.roommateName || null, roommateSingleAgree: scope.body.roommateSingleAgree ? 1 : 0, name: scope.body.name, passportNumber: scope.body.passportNumber, passportIssuedDate: new Date(scope.body.passportIssuedDate), passportExpiredDate: new Date(scope.body.passportExpiredDate), passportAuthority: scope.body.passportAuthority, nationality: scope.body.nationality, } let data: undefined | LiveAmsterdamRegistrationData if (existingRegistration) { // π update await db .update(amsterdamRegistrations) .set(upsert) .where(eq(amsterdamRegistrations.id, existingRegistration.id)) data = { // data is for Ace Live Server message: `${scope.body.name} updated their Amsterdam Registration!`, registration: await dbGetAmsterdamRegistration(eq(amsterdamRegistrations.id, existingRegistration.id)), } } else { // β¨ create const [resultSet] = await db .insert(amsterdamRegistrations) .values(upsert) .returning() data = { // data is for Ace Live Server message: `${scope.body.name} created their Amsterdam Registration!`, registration: await dbGetAmsterdamRegistration(eq(amsterdamRegistrations.id, resultSet.id)) } } await scope.liveEvent({ // broadcast (echo) live event to all subscribers of the amsterdamRegistration stream data, stream: streams.keys.amsterdamRegistration, requestInit: { headers: { LIVE_SECRET: process.env.LIVE_SECRET } } }) return scope.success(`Registration ${existingRegistration ? 'updated' : 'saved'}!`) })Component.tsximport { showToast } from '@ace/toast' import { onCleanup, onMount } from 'solid-js' import { LiveAmsterdamRegistrationData } from '@src/lib/types' const {sync} = useStore() onMount(() => { const ws = scope.liveSubscribe({ stream: streams.keys.amsterdamRegistration }) // π subscribe to amsterdamRegistration stream ws.addEventListener('message', async (event) => { // π onMessage const data: LiveAmsterdamRegistrationData = JSON.parse(event.data) // parse data showToast({ type: 'success', value: data.message }) // show notfication sync('apiGetAmsterdamRegistration', data.registration) // update FE data }) onCleanup(() => scope.liveUnsubscribe(ws)) // π on leave => unsubscribe })Ace Live Server>index.tsimport { jwtValidate } from '@ace/jwtValidate' import { createLiveWorker, createLiveDurableObject, readCookie } from '@ace/liveServer' export default createLiveWorker() satisfies ExportedHandler<Env> export const LiveDurableObject = createLiveDurableObject({ onValidateEvent(request) { // only our server can post events if (request.headers.get('LIVE_SECRET') !== process.env.LIVE_SECRET) { return new Response('Unauthorized', { status: 400 }) } }, async onValidateSubscribe(request) { // only our signed in customers can subscribe (we may share cookies if we put our live server on a subdomain of the app server (ex: live.example.com) const jwt = readCookie(request, 'aceJWT') const res = await jwtValidate({ jwt }) if (!res.isValid) return new Response('Unauthorized', { status: 400 }) } })
- Why doesen't
Aceuserevalidate()?- Solid's
revalidate()relies on signals created byquery()andcreateAsync() - To update
statewithAce, we don't updatequerysignals, we updatestatesignals
- Solid's
- Why doesen't
AceuseSingle Flight Mutations?- The
Responsethat comes from aSingle Flight Mutationis framework specific & not aJSONshape we define - & b/c of how
RPCfunctions are created, theRequest URLmay also be difficult to predict - We love defined & typesafe,
URLs&Responses
- The
Breakpoints
BE Breakpoints β
- @
BEcode, place adebuggerw/in your code and/or anif (condition) debugger(as seen in screenshot below) - Refresh site and now in your editor you may
watch variables& see thecall stack!
FE Breakpoints β
- @
FEcode, place adebuggerw/in your code and/or anif (condition) debugger(as seen in screenshot below) - In browser navigate to
Inpect>Sources - Refresh site and now in your browser you may
watch variables& see thecall stack!
Scope
β ScopeBE
- Available @:
B4Functions, example:export const sessionB4: B4<{ session: Session }> = async (scope) => { scope.event.locals.session = await getSession(scope) }API>.resolve()Functions, example:import { API } from '@ace/api' import { eq } from 'drizzle-orm' import { db, users } from '@src/lib/db' import { sessionB4 } from '@src/auth/authB4' import { updateEmailParser } from '@src/parsers/updateEmailParser' export const POST = new API('/api/update-email', 'apiUpdateEmail') .b4([sessionB4]) .body(updateEmailParser) .resolve(async (scope) => { await db .update(users) .set({ email: scope.body.email }) .where(eq(users.id, scope.event.locals.session.userId)) return scope.success() })
- Features:
scope.respond()- Builds a
Responsebased on the provided props scope.success(),scope.error()&scope.go()each usescope.respond()- The option to return a custom
Responsefrom anAPIorB4is available too btw - Props:
{ path?: T_Path, // redirect path string data?: T_Data, // object status?: number, error?: AceError, headers?: HeadersInit, pathParams?: RoutePath2PathParams<T_Path>, // object (for redirect) searchParams?: RoutePath2SearchParams<T_Path>, // object (for redirect) }
- Builds a
scope.success()- Creates a success
Responsew/ simple options - For all options please call
scope.respond() - Props:
(data?: T_Data, status = 200)
- Creates a success
scope.error()- Creates a error
Responsew/ simple options - For all options please call
scope.respond() - Props:
(message: string, status = 400)
- Creates a error
scope.go()- Creates a redirect
Responsew/ simple options - For all options please call
scope.respond() - Props:
(path: T_Path, params?: { pathParams?: RoutePath2PathParams<T_Path>, searchParams?: RoutePath2SearchParams<T_Path> })
- Creates a redirect
scope.setCookie()- Set a cookie
- Props:
(name: string, value: string, options?: CookieSerializeOptions)
scope.getCookie()- Get a cookie value by name
- Props:
(name: string)
scope.clearCookie()- Delete a cookie by name
- Props:
(name: string)
scope.liveEvent()- Helpful when you'd love to create an Ace Live Server
event
- Helpful when you'd love to create an Ace Live Server
scope.requestUrlOrigin- The origin of the current HTTP request URL
β ScopeComponent
- Available @:
- Any component via
import { scope } from '@ace/scopeComponent' Layout > .component(), example:import { Nav } from '@src/Nav/Nav' import { Layout } from '@ace/layout' export default new Layout() .component(({ children }) => { return <> <Nav /> {children} </> })Route > .component(), example:import { Route } from '@ace/route' export default new Route('/') .component((scope) => { return <> <Show when={scope.bits.get('apiExample') === false} fallback={<Loading />}> <Loaded /> </Show> </> })Route404 > .component(), example:import { Route404 } from '@ace/route404' export default new Route404() .component((scope) => { return <> <h1>{scope.location.pathname}</h1> </> })
- Any component via
- Features:
scope.pathParams- Path params as an object
- If using in a
createEffect()THEN usescope.PathParams()
scope.PathParams()- Reactive path params that can be used in a
createEffect() - If not using in a
createEffect()THEN usescope.pathParams
- Reactive path params that can be used in a
scope.searchParams- Search params as an object
- If using in a
createEffect()THEN usescope.SearchParams()
scope.SearchParams()- Reactive search params that can be used in a
createEffect() - If not using in a
createEffect()THEN usescope.searchParams
- Reactive search params that can be used in a
scope.location- Location as an object
- If using in a
createEffect()THEN usescope.Location()
scope.Location()- Reactive location that can be used in a
createEffect() - IF not using in a
createEffect()THEN usescope.location
- Reactive location that can be used in a
scope.liveSubscribe()- Helpful when you'd love to create a ws connection to an Ace Live Server
- Example:
const ws = scope.liveSubscribe({ stream: 'example' }) ws.addEventListener('message', event => { console.log(event.data) }) ws.addEventListener('close', () => { console.log('ws closed') })
scope.go()- Frontend redirect w/ simple options
- For all possible options please use
scope.Go() - Props:
/** * @param path - Redirect to this path, as defined @ new Route() * @param params.pathParams - Path params * @param params.searchParams - Search params */
scope.Go()- Frontend redirect w/ simple options
- For all possible options please use
scope.Go() - Props:
/** * @param path - Redirect to this path, as defined @ new Route() * @param pathParams - Path params * @param searchParams - Search params * @param replace - Optional, defaults to false, when true this redirect will clear out a history stack entry * @param scroll - Optional, defaults to true, if you'd like to scroll to the top of the page when done redirecting * @param state - Optional, defaults to an empty object, must be an object that is serializable, available @ the other end via `fe.getLocation().state` */
scope.children- Get the children for a layout
- IF not a layout OR no children THEN
undefined
scope.GET()- Call api
GETmethod w/ typesafe autocomplete - Props:
/** * @param path - As defined @ `new API()` * @param options.bitKey - `Bits` are `boolean signals`, they live in a `map`, so they each have a `bitKey` to help us identify them, the provided bitKey will have a value of true while this api is loading * @param options.pathParams - Path params * @param options.searchParams - Search params * @param options.manualBitOff - Optional, defaults to false, set to true when you don't want the bit to turn off in this function but to turn off in yours, helpful if you want additional stuff to happen afte api call then say we done */
- Call api
scope.POST()- Call api
POSTmethod w/ typesafe autocomplete - Props:
/** * @param path - As defined @ `new API()` * @param options.bitKey - `Bits` are `boolean signals`, they live in a `map`, so they each have a `bitKey` to help us identify them, the provided bitKey will have a value of true while this api is loading * @param options.pathParams - Path params * @param options.searchParams - Search params * @param options.body - Request body * @param options.manualBitOff - Optional, defaults to false, set to true when you don't want the bit to turn off in this function but to turn off in yours, helpful if you want additional stuff to happen afte api call then say we done */
- Call api
scope.PUT()- Call api
PUTmethod w/ typesafe autocomplete - Props:
/** * @param path - As defined @ `new API()` * @param options.bitKey - `Bits` are `boolean signals`, they live in a `map`, so they each have a `bitKey` to help us identify them, the provided bitKey will have a value of true while this api is loading * @param options.pathParams - Path params * @param options.searchParams - Search params * @param options.body - Request body * @param options.manualBitOff - Optional, defaults to false, set to true when you don't want the bit to turn off in this function but to turn off in yours, helpful if you want additional stuff to happen afte api call then say we done */
- Call api
scope.DELETE()- Call api
DELETEmethod w/ typesafe autocomplete - Props:
/** * @param path - As defined @ `new API()` * @param options.bitKey - `Bits` are `boolean signals`, they live in a `map`, so they each have a `bitKey` to help us identify them, the provided bitKey will have a value of true while this api is loading * @param options.pathParams - Path params * @param options.searchParams - Search params * @param options.body - Request body * @param options.manualBitOff - Optional, defaults to false, set to true when you don't want the bit to turn off in this function but to turn off in yours, helpful if you want additional stuff to happen afte api call then say we done */
- Call api
Create a 404 Route
import './Unknown.css'
import { A } from '@ace/a'
import { Title } from '@solidjs/meta'
import { Route404 } from '@ace/route404'
import RootLayout from '@src/app/RootLayout'
export default new Route404()
.layouts([RootLayout]) // zero to many layouts available @ Route or Route404!
.component(({location}) => {
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">{location.pathname}</div>
<A path="/" class="brand">π‘ Go Back Home</A>
</main>
</>
})Create a Typesafe Anchor
- Path, pathParams & searchParams = typesafe
- So IF path or search params are defined for this path as required THEN they'll be required here via TS IDE autocomplete intellisense
- By default & configurable this component toggles an active class based on the current route π₯³
<A path="/about">Learn More</A>Typesafe Redirects
- Redirect @
B4!import type { B4 } from '@ace/types' export const redirectB4: B4 = async (scope) => { return scope.go('/') // return or throw both work :) } - Redirect @
API > .resolve()// scope.go() and scope.error() are shortcuts to scope.respond() // IF scope.go() does not have all the options you want (ex: custom status or headers) please use scope.respond({ path: '/', status: abc, headers: xyz }) import { API } from '@ace/api' import { vParse } from '@ace/vParse' import { vEnums } from '@ace/vEnums' import { elements } from 'src/lib/vars' import { object, picklist } from 'valibot' export const GET = new API('/api/element/:element', 'apiElement') .pathParams(vParse(object({ element: vEnums(elements) }))) .resolve(async (scope) => { return scope.pathParams.element === 'fire' // β¨ Typesafe API Path Params ? scope.error('π₯') : scope.go('/') // π« Typesafe API Redirects (throwing scope.go() here works too) }) - Redirect @
Route > .component()!// IF scope.go() does not have all the options you want (ex: custom scroll or replace) please use scope.Go({ path: '/example', replace: true }) // π¨ Throw the `go()` or `Go()`, THROWING thankfully ends the inference loop of defining and returning a route π import { Route } from '@ace/route' export default new Route('/') .component(scope => { throw scope.go('/deposits') })
Add Offline Support
- Create
/public/sw.js// @ts-check import { swAddOffLineSupport } from './.ace/swAddOffLineSupport.js' const packageDotJsonVersion = '' swAddOffLineSupport({ cacheName: `offline-cache-v-${packageDotJsonVersion}` }) - π¨ To align our app version (in package.json) w/ your cache version then:
- Ensure
package.jsonversion is defined - Ensure
/public/sw.jshasconst packageDotJsonVersion = '' - Run in bash:
ace swto place yourpackage.jsonversion into your sw.js file π₯³ - Update package.json scripts to run
ace swautomatically{ "scripts": { "dev": "ace build local && ace sw && vinxi dev", "build": "ace build prod && ace sw && vinxi build", }, }
- Ensure
- @
src/entry-server.tsxadd@ace/sw.styles.css?rawAND@ace/swRegister?raw. The?rawis aVitething, it gives us the import as a string, and then we inline it for an optimal Lighthouse score!// @refresh reload import swRegister from '@ace/swRegister?raw' import swStyles from '@ace/sw.styles.css?raw' import { createHandler, StartServer } from '@solidjs/start/server' export default createHandler(() => ( <StartServer document={({ assets, children, scripts }) => ( <html lang="en"> <head> <meta charset="utf-8" /> <style>{swStyles}</style> <link rel="icon" href="/favicon.ico" /> <link rel="manifest" href="/manifest.json" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="description" content="Create Ace App is a showcase for what is possible w/ Ace!" /> {/* enhance how the site looks when added to an iOS device's home screen */} <meta name="apple-mobile-web-app-capable" content="yes" /> {/* tells iOS Safari that the web application should be run in full-screen mode without the standard browser chrome (address bar, toolbar) when launched from a home screen icon */} <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> {/* sets the appearance of the iOS status bar (time, battery icons) when running in full-screen mode */} {assets} </head> <body> <div id="app">{children}</div> <script>{swRegister}</script> {scripts} </body> </html> )} /> ))
- How it works:
- On every GET request we first go to the server, and if that response is ok then the service worker will store the response in cache
- When offline we'll still try to fetch, and when it fails we'll check cache, and if cache has a response we'll give it
- The styling is b/c service workers register when you go to a page that has a service worker, but it won't install and activate till the first refresh after it's been registered, so we do this immediately after registration, and to not let the user know this is happening we have the app be 0 opacity till after the refresh. The refresh only happens once in the lifetime of the customer on the app w/ this cache version (app version).
Network Status Hook
- Helpful if you'd love to alter things based on their network (wifi) connection
- IF hook called one or many times THEN still only 1 event is added to window to watch network status & then this value is shared w/ all hooks
import { Route } from '@ace/route' import { showToast } from '@ace/toast' import { useNetworkStatus } from '@ace/useNetworkStatus' export default new Route('/') .component(() => { const status = useNetworkStatus() // Accessor<'online' | 'offline'> return <> <button type="button" onClick={() => showToast({ type: 'info', value: status() })}>Click here</button> </> })
Create Desktop Application
β
npx create-ace-app@latest is built w/ the following directions done btw!

- Please follow the Add Offline Support directions to ensure you register the service worker correctly!
- Offline support is a lovely app feature but if you don't want it, just don't call
swAddOffLineSupport()@/public/sw.js
- Offline support is a lovely app feature but if you don't want it, just don't call
- For free in Figma create a 512x512 icon for your app
- For free @ Progressier create a
manifest.jsonand icon suite - Add the generated
manifest.jsonandiconsto your/publicfolder - @
src/entry-server.tsx><head>add<link rel="manifest" href="/manifest.json" /> - App install is now ready! (works on
localhosttoo btw!)
Markdown
<AceMarkdown /> β
- Ideal for SEO
- Supports
.mdfiles & markdownPreview@ VsCodium β
- Example:
// How `.md?raw` works: // at build-time, the markdown file is minified & bundled as a string literal // so at run-time, there's no file I/O b/c the markdown is an in memory string constant // π¨ IF not highlighting code THEN `registerHljs` AND `hljsMarkdownItOptions` are not necessary import { AceMarkdown } from '@ace/aceMarkdown' import { Example } from '@src/Example/Example' import mdAppInfo from '@src/md/mdAppInfo.md?raw' import { registerHljs } from '@src/init/registerHljs' import { hljsMarkdownItOptions } from '@ace/hljsMarkdownItOptions' <AceMarkdown content={mdAppInfo} components={[Example]} registerHljs={registerHljs} markdownItOptions={{ highlight: hljsMarkdownItOptions }} /> - Requires:
npm i markdown-it -D - Converts
markdowntohtmlAND supportsDirectives, added viacommentsthat start with<!--{, end with}-->and includeJSONw/in Directives:$info:- Example:
<!--{ "$info": true, "$interpolate": true, "title": "What is Ace?", "slug": "what-is-ace" }--> - IF
$info.$interpolateistrueTHENvarsfrom$infocan be placed @markdown,component props,tab label&tab content
- Example:
$component:- Adds a Solid component into markdown π
- Example:
<!--{ "$component": "Example", "title": "{title}" }--> - THEN pass the
component functiontoAceMarkdown, example:<AceMarkdown content={mdWhatIsAce} components={[Example]} />
- β€οΈ
$tabs:- Example:
<!--{ "$tabs": ["TS", "JS"] }--> - The value in the directive @
$tabsis the tablabels, so for this example there would be 2 labels, one isTSand the other isJS - Place the tab content below the
Directive - π¨ Add
---at the end of each tab content to let us know where each tab content ends - IF the number of
content itemsdoes not match the number oflabelsTHEN we'll throw an error - β
Tab content may include,
markdown, orcomponents - W/in the
Directivethese additionalTabsPropscan be provided of{ name?: string, variant?: 'underline' | 'pill' | 'classic', $div?: JSX.HTMLAttributes<HTMLDivElement> }- Example:
<!--{ "$tabs": ["TS", "JS"], "variant": "underline" }-->
- Example:
- Example:
- Props:
{ /** Ace Markdown Content, example: `import mdWhatIsAce from '@src/md/mdWhatIsAce.md?raw'`, then provide `mdWhatIsAce` here */ content: string, /** Components that are in the AceMarkdown, just passing the function names is perfect */ components?: (() => JSX.Element)[], /** Helpful when you'd love the markdownIt instance, example: `const [md, setMD] = createSignal<MarkdownIt>()` and then pass `setMD` here */ setMD?: Setter<markdownit | undefined> /** Optional, requested options will be merged w/ the `defaultMarkdownOptions` */ markdownItOptions?: MarkdownItOptions /** Optional, props passed to inner wrapper div */ $div?: JSX.HTMLAttributes<HTMLDivElement>, /** Optional, to enable code highlighting pass a function here that registers highlight languages */ registerHljs?: () => void, }
parseMarkdownFolders() β
- Helpful when we have a folder of
.mdfiles and we'd love to gettypesafeinfo about all of the markdowns w/in the folder - Example: We've got a folder for posts and we wanna show each post title & link to it w/in a component
Ace Markdown Directive:<!--{ "$info": true, "title": "What is Ace?", "slug": "what-is-ace" }-->ace.config.jsexport const config = { mdFolders: [ { id: 'mdFolder', path: 'src/md' }, { id: 'contentFolder', path: 'src/content' }, ], }./src/parsers/mdParserexport const mdParser = vParse(object({ $info: vBool(), slug: vString(), title: vString() }))Componentimport { parseMarkdownFolders } from '@ace/parseMarkdownFolders' const mdFolder = parseMarkdownFolders('mdFolder', mdParser) const contentFolder = parseMarkdownFolders('contentFolder', parser) console.log({ mdFolder, contentFolder, // { path: string, raw: string, info: T }[] whatIsAce: mdFolder.find(md => md.info.slug === 'what-is-ace') })
<MarkdownItStatic /> β
- Ideal for SEO
- Does not support
<AceMarkdown />directives - Supports
.mdfiles & markdownPreview@ VsCodium β - Install:
npm i markdown-it -D - Example:
// π¨ IF not highlighting code THEN `registerHljs` AND `hljsMarkdownItOptions` are not necessary import mdAppInfo from '@src/md/mdAppInfo.md?raw' import { registerHljs } from '@src/init/registerHljs' import { MarkdownItStatic } from '@ace/markdownItStatic' import { hljsMarkdownItOptions } from '@ace/hljsMarkdownItOptions' <MarkdownItStatic content={mdAppInfo} registerHljs={registerHljs} options={{ highlight: hljsMarkdownItOptions }} /> - Props:
{ /** Content to render from markdown to html, can also pass content later by updating the passed in content prop or `md()?.render()` */ content: string, /** Helpful when you'd love the markdownIt instance, example: `const [md, setMD] = createSignal<MarkdownIt>()` and then pass `setMD` here */ setMD?: Setter<markdownit | undefined> /** Optional, requested options will be merged w/ the `defaultMarkdownOptions` */ markdownItOptions?: MarkdownItOptions /** Optional, props passed to inner wrapper div */ $div?: JSX.HTMLAttributes<HTMLDivElement>, /** Optional, to enable code highlighting pass a function here that registers highlight languages */ registerHljs?: () => void }
<MarkdownItDynamic /> β
- Ideal for dynamic markdown (from
DB) orFEalterable markdown (fromtextarea) - Does not support
<AceMarkdown />directives - Install:
npm i markdown-it -D - Example:
// π¨ IF not highlighting code THEN `registerHljs` AND `hljsMarkdownItOptions` are not necessary import { registerHljs } from '@src/init/registerHljs' import { MarkdownItDynamic } from '@ace/markdownItDynamic' import { hljsMarkdownItOptions } from '@ace/hljsMarkdownItOptions' <MarkdownItDynamic content={() => store.buildStats} registerHljs={registerHljs} options={{ highlight: hljsMarkdownItOptions }} $div={{ class: 'markdown' }} /> - Props:
{ /** Content to render from markdown to html, can also pass content later by updating the passed in content prop or `md()?.render()` */ content: Accessor<string | undefined> /** Helpful when you'd love the markdownIt instance, example: `const [md, setMD] = createSignal<MarkdownIt>()` and then pass `setMD` here */ setMD?: Setter<markdownit | undefined> /** Optional, requested options will be merged w/ the `defaultMarkdownOptions` */ markdownItOptions?: MarkdownItOptions /** Optional, props passed to inner wrapper div */ $div?: JSX.HTMLAttributes<HTMLDivElement>, /** Optional, to enable code highlighting pass a function here that registers highlight languages */ registerHljs?: () => void }
defaultMarkdownOptions β
- Requested
markdownItOptions@<AceMarkdown />,<MarkdownItStatic />&<MarkdownItDynamic />will be merged w/ thesedefaultMarkdownOptionsimport type { Options as MarkdownItOptions } from 'markdown-it' const defaultMarkdownOptions: MarkdownItOptions = { html: true, linkify: true, typographer: true }
