From b029de20e6c439847900796707f2cddebc0942e2 Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Sun, 11 Oct 2026 03:39:24 +0100 Subject: [PATCH 1/3] feat(realtime): add dynamic SDK loading support for transports and device tokens --- CHANGELOG.md | 1 + docs/guide/notifications.md | 18 ++++ packages/realtime/src/RealtimeClient.ts | 8 +- packages/realtime/src/importer.ts | 10 ++ packages/realtime/src/push.ts | 12 +-- .../src/transports/firebase-database.ts | 15 ++- packages/realtime/src/transports/firebase.ts | 20 ++-- packages/realtime/src/transports/pusher.ts | 10 +- packages/realtime/src/types.ts | 9 ++ packages/realtime/tests/import-module.test.ts | 94 +++++++++++++++++++ 10 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 packages/realtime/src/importer.ts create mode 100644 packages/realtime/tests/import-module.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb23a72..2264bd68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format follows semantic versioning principles. ### Added +- `RealtimeConfig.importModule` loads the transport SDKs (`pusher-js`, `firebase/*`) for every transport and for device tokens, so pages without a bundler can load them from a CDN. Defaults to a dynamic `import()`. The `createPusherTransport`, `createFirebaseTransport`, `createFirebaseDatabaseTransport` and `requestDeviceToken` helpers accept it as an extra argument. - `RealtimeClient.deviceToken({ serviceWorker })` takes a service worker registration or script URL (registered with the Firebase web config as `firebaseConfig`), returns `null` where web push is unsupported, and falls back to the `firebase` config when streaming over another transport. `requestDeviceToken()` issues a token without a client. - Added the `Broadcast` facade to `@arkstack/notifications`: a channel authorization registry (`Broadcast.channel('private-team.{teamId}', authorizer)`) used by every transport, `Broadcast.broadcast()` for arbitrary events, `Broadcast.handleAuth()` for custom auth routes, `Broadcast.clientConfig()`, and `Broadcast.routes()` registering `POST /realtime/auth` and `GET /realtime/config`. - Added the `firebase-database` realtime transport (Firebase Realtime Database channels with per-user, time-limited grants checked by security rules, plus bounded `prune()`), the signal-only `polling` transport, and the `memory` transport for tests, on both server and client. diff --git a/docs/guide/notifications.md b/docs/guide/notifications.md index d8826e08..963c1e99 100644 --- a/docs/guide/notifications.md +++ b/docs/guide/notifications.md @@ -438,6 +438,24 @@ await realtime.onPoll(refresh); For tests, use `transport: 'memory'` and feed subscribers with `emitMemoryEvent(channel, event, payload)`. +The transport SDKs are loaded with a dynamic `import()` of their package name (`pusher-js`, `firebase/app`, …), which a bundler resolves. On a page without a bundler, such as a plain `
` + return `
` } /** Minimal built-in root document used when the configured root view is absent. */ export const builtInTemplate = (mount: string, head: string = ''): string => { - return [ - '', - '', - '', - '', - '', - ...(head ? [head] : []), - '', - '', - mount, - '', - '', - ].join('\n') + return [ + '', + '', + '', + '', + '', + ...(head ? [head] : []), + '', + '', + mount, + '', + '', + ].join('\n') } /** @@ -55,39 +55,36 @@ export const builtInTemplate = (mount: string, head: string = ''): string => { * `root_view` Edge template (which receives `inertia` and `inertiaHead` * variables), or a minimal built-in document when that view is absent. */ -export const renderRootHtml = async ( - page: InertiaPage, - config: InertiaConfig, -): Promise => { - let mount = renderDataPage(page, config.root_id) - let head = '' +export const renderRootHtml = async (page: InertiaPage, config: InertiaConfig): Promise => { + let mount = renderDataPage(page, config.root_id) + let head = '' - if (config.ssr?.enabled) { - const ssr = await renderViaSsr(page, config.ssr.url) + if (config.ssr?.enabled) { + const ssr = await renderViaSsr(page, config.ssr.url) - if (ssr) { - mount = ssr.body - head = ssr.head.join('\n') - } + if (ssr) { + mount = ssr.body + head = ssr.head.join('\n') } + } - try { - const { view } = await import('@arkstack/view') - const { registerInertiaTags } = await import('./tags') - const factory = view() + try { + const { view } = await import('@arkstack/view') + const { registerInertiaTags } = await import('./tags') + const factory = view() - registerInertiaTags(factory) + registerInertiaTags(factory) - if (factory.exists(config.root_view)) { - return await view(config.root_view, { - page, - inertia: mount, - inertiaHead: head, - }).render() - } - } catch { - /** View layer unavailable; fall back to the built-in document. */ + if (factory.exists(config.root_view)) { + return await view(config.root_view, { + page, + inertia: mount, + inertiaHead: head, + }).render() } + } catch { + /** View layer unavailable; fall back to the built-in document. */ + } - return builtInTemplate(mount, head) + return builtInTemplate(mount, head) } diff --git a/packages/inertia/src/props.ts b/packages/inertia/src/props.ts index d604d27e..9d9037b8 100644 --- a/packages/inertia/src/props.ts +++ b/packages/inertia/src/props.ts @@ -1,9 +1,4 @@ -import type { - AlwaysPropContract, - DeferPropContract, - InertiaPropWrapper, - LazyPropContract, -} from './types' +import type { AlwaysPropContract, DeferPropContract, InertiaPropWrapper, LazyPropContract } from './types' /** * A prop that is excluded from the initial page load and only resolved when the @@ -11,13 +6,13 @@ import type { * first render does not need. */ export class LazyProp implements LazyPropContract { - readonly __inertia = 'lazy' as const + readonly __inertia = 'lazy' as const - constructor(private readonly callback: () => unknown) {} + constructor(private readonly callback: () => unknown) {} - call () { - return this.callback() - } + call() { + return this.callback() + } } /** @@ -25,13 +20,13 @@ export class LazyProp implements LazyPropContract { * do not list it — and cannot be filtered out by `only`/`except`. */ export class AlwaysProp implements AlwaysPropContract { - readonly __inertia = 'always' as const + readonly __inertia = 'always' as const - constructor(private readonly value: unknown) {} + constructor(private readonly value: unknown) {} - call () { - return typeof this.value === 'function' ? (this.value as () => unknown)() : this.value - } + call() { + return typeof this.value === 'function' ? (this.value as () => unknown)() : this.value + } } /** @@ -40,30 +35,33 @@ export class AlwaysProp implements AlwaysPropContract { * fetched together in a single request. */ export class DeferProp implements DeferPropContract { - readonly __inertia = 'defer' as const + readonly __inertia = 'defer' as const - constructor(private readonly callback: () => unknown, readonly group: string = 'default') {} + constructor( + private readonly callback: () => unknown, + readonly group: string = 'default', + ) {} - call () { - return this.callback() - } + call() { + return this.callback() + } } /** Narrow an arbitrary value to one of the Inertia prop wrappers. */ export const isPropWrapper = (value: unknown): value is InertiaPropWrapper => { - return Boolean( - value - && typeof value === 'object' - && '__inertia' in value - && typeof (value as { call?: unknown }).call === 'function', - ) + return Boolean( + value && + typeof value === 'object' && + '__inertia' in value && + typeof (value as { call?: unknown }).call === 'function', + ) } export const isLazyProp = (value: unknown): value is LazyPropContract => - isPropWrapper(value) && value.__inertia === 'lazy' + isPropWrapper(value) && value.__inertia === 'lazy' export const isAlwaysProp = (value: unknown): value is AlwaysPropContract => - isPropWrapper(value) && value.__inertia === 'always' + isPropWrapper(value) && value.__inertia === 'always' export const isDeferProp = (value: unknown): value is DeferPropContract => - isPropWrapper(value) && value.__inertia === 'defer' + isPropWrapper(value) && value.__inertia === 'defer' diff --git a/packages/inertia/src/protocol.ts b/packages/inertia/src/protocol.ts index 5beb395e..53c2bfe4 100644 --- a/packages/inertia/src/protocol.ts +++ b/packages/inertia/src/protocol.ts @@ -7,5 +7,5 @@ export const SEE_OTHER_METHODS = new Set(['PUT', 'PATCH', 'DELETE']) * `GET`, which is required after `PUT`/`PATCH`/`DELETE` mutations. */ export const shouldUpgradeRedirect = (method: string, status: number): boolean => { - return status === 302 && SEE_OTHER_METHODS.has(method.toUpperCase()) + return status === 302 && SEE_OTHER_METHODS.has(method.toUpperCase()) } diff --git a/packages/inertia/src/resolve.ts b/packages/inertia/src/resolve.ts index d3d5786c..a49a5442 100644 --- a/packages/inertia/src/resolve.ts +++ b/packages/inertia/src/resolve.ts @@ -3,45 +3,48 @@ import type { InertiaRequest, PageProps } from './types' /** Parse a comma-separated partial-reload header into a trimmed, non-empty list. */ const csv = (value: string | undefined): string[] => { - if (!value) { - return [] - } - - return value.split(',').map(entry => entry.trim()).filter(Boolean) + if (!value) { + return [] + } + + return value + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) } /** Recursively evaluate prop wrappers and callbacks within a value. */ const evaluate = async (value: unknown): Promise => { - if (isPropWrapper(value)) { - return evaluate(value.call()) - } + if (isPropWrapper(value)) { + return evaluate(value.call()) + } - if (typeof value === 'function') { - return evaluate((value as () => unknown)()) - } + if (typeof value === 'function') { + return evaluate((value as () => unknown)()) + } - if (value && typeof (value as PromiseLike).then === 'function') { - return evaluate(await value) - } + if (value && typeof (value as PromiseLike).then === 'function') { + return evaluate(await value) + } - if (Array.isArray(value)) { - return Promise.all(value.map(entry => evaluate(entry))) - } + if (Array.isArray(value)) { + return Promise.all(value.map((entry) => evaluate(entry))) + } - if (value && typeof value === 'object' && (value as object).constructor === Object) { - const entries = await Promise.all( - Object.entries(value as PageProps).map(async ([key, val]) => [key, await evaluate(val)] as const), - ) + if (value && typeof value === 'object' && (value as object).constructor === Object) { + const entries = await Promise.all( + Object.entries(value as PageProps).map(async ([key, val]) => [key, await evaluate(val)] as const), + ) - return Object.fromEntries(entries) - } + return Object.fromEntries(entries) + } - return value + return value } export interface ResolvedProps { - props: PageProps - deferredProps?: Record + props: PageProps + deferredProps?: Record } /** @@ -58,52 +61,52 @@ export interface ResolvedProps { * response so the client can request them afterwards. */ export const resolveProps = async ( - component: string, - props: PageProps, - request: InertiaRequest, + component: string, + props: PageProps, + request: InertiaRequest, ): Promise => { - const isInertia = request.header('x-inertia') === 'true' - const isPartial = isInertia && request.header('x-inertia-partial-component') === component - const only = csv(request.header('x-inertia-partial-data')) - const except = csv(request.header('x-inertia-partial-except')) - - const deferred: Record = {} - const included: PageProps = {} - - for (const [key, value] of Object.entries(props)) { - let include: boolean - - if (!isPartial) { - // Collect deferred props for the client to fetch after the initial load. - if (isDeferProp(value)) { - (deferred[value.group] ??= []).push(key) - } - - include = !isLazyProp(value) && !isDeferProp(value) - } else { - include = only.length ? only.includes(key) : true - - if (except.length && except.includes(key)) { - include = false - } - - // Deferred props are only sent on a partial that explicitly asks for them. - if (isDeferProp(value) && !only.includes(key)) { - include = false - } - } - - // `always` props ignore every filter. - if (isAlwaysProp(value)) { - include = true - } - - if (include) { - included[key] = await evaluate(value) - } + const isInertia = request.header('x-inertia') === 'true' + const isPartial = isInertia && request.header('x-inertia-partial-component') === component + const only = csv(request.header('x-inertia-partial-data')) + const except = csv(request.header('x-inertia-partial-except')) + + const deferred: Record = {} + const included: PageProps = {} + + for (const [key, value] of Object.entries(props)) { + let include: boolean + + if (!isPartial) { + // Collect deferred props for the client to fetch after the initial load. + if (isDeferProp(value)) { + ;(deferred[value.group] ??= []).push(key) + } + + include = !isLazyProp(value) && !isDeferProp(value) + } else { + include = only.length ? only.includes(key) : true + + if (except.length && except.includes(key)) { + include = false + } + + // Deferred props are only sent on a partial that explicitly asks for them. + if (isDeferProp(value) && !only.includes(key)) { + include = false + } + } + + // `always` props ignore every filter. + if (isAlwaysProp(value)) { + include = true + } + + if (include) { + included[key] = await evaluate(value) } + } - const deferredProps = !isPartial && Object.keys(deferred).length ? deferred : undefined + const deferredProps = !isPartial && Object.keys(deferred).length ? deferred : undefined - return { props: included, deferredProps } + return { props: included, deferredProps } } diff --git a/packages/inertia/src/setup.ts b/packages/inertia/src/setup.ts index cf417373..af32d704 100644 --- a/packages/inertia/src/setup.ts +++ b/packages/inertia/src/setup.ts @@ -15,121 +15,119 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..') * (Vite tags, title, meta, etc.). */ Publisher.publishes({ - package: '@arkstack/inertia', - tag: 'inertia-config', - entries: [ - { - from: join(root, 'stubs/config/inertia.ts.stub'), - to: 'src/config/inertia.ts', - }, - { - from: join(root, 'stubs/config/app.css.stub'), - to: 'resources/css/app.css', - }, - ], + package: '@arkstack/inertia', + tag: 'inertia-config', + entries: [ + { + from: join(root, 'stubs/config/inertia.ts.stub'), + to: 'src/config/inertia.ts', + }, + { + from: join(root, 'stubs/config/app.css.stub'), + to: 'resources/css/app.css', + }, + ], }) Publisher.publishes({ - package: '@arkstack/inertia', - tag: 'inertia-views', - entries: [ - { - from: join(root, 'stubs/views/app.edge.stub'), - to: 'src/resources/views/app.edge', - }, - ], + package: '@arkstack/inertia', + tag: 'inertia-views', + entries: [ + { + from: join(root, 'stubs/views/app.edge.stub'), + to: 'src/resources/views/app.edge', + }, + ], }) Publisher.publishes({ - package: '@arkstack/inertia', - tag: 'inertia-react', - entries: [ - { - from: join(root, 'stubs/react/resources/js/app.tsx.stub'), - to: 'resources/js/app.tsx', - }, - { - from: join(root, 'stubs/react/vite.config.ts.stub'), - to: 'vite.config.ts', - }, - { - from: join(root, 'stubs/react/resources/js/Pages/Index.tsx.stub'), - to: 'resources/js/Pages/Index.tsx', - }, - { - from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), - to: 'resources/js/vite-env.d.ts', - }, - ], + package: '@arkstack/inertia', + tag: 'inertia-react', + entries: [ + { + from: join(root, 'stubs/react/resources/js/app.tsx.stub'), + to: 'resources/js/app.tsx', + }, + { + from: join(root, 'stubs/react/vite.config.ts.stub'), + to: 'vite.config.ts', + }, + { + from: join(root, 'stubs/react/resources/js/Pages/Index.tsx.stub'), + to: 'resources/js/Pages/Index.tsx', + }, + { + from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), + to: 'resources/js/vite-env.d.ts', + }, + ], }) Publisher.publishes({ - package: '@arkstack/inertia', - tag: 'inertia-svelte', - entries: [ - { - from: join(root, 'stubs/svelte/resources/js/app.ts.stub'), - to: 'resources/js/app.ts', - }, - { - from: join(root, 'stubs/svelte/vite.config.ts.stub'), - to: 'vite.config.ts', - }, - { - from: join(root, 'stubs/svelte/resources/js/Pages/Index.svelte.stub'), - to: 'resources/js/Pages/Index.svelte', - }, - { - from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), - to: 'resources/js/vite-env.d.ts', - }, - ], + package: '@arkstack/inertia', + tag: 'inertia-svelte', + entries: [ + { + from: join(root, 'stubs/svelte/resources/js/app.ts.stub'), + to: 'resources/js/app.ts', + }, + { + from: join(root, 'stubs/svelte/vite.config.ts.stub'), + to: 'vite.config.ts', + }, + { + from: join(root, 'stubs/svelte/resources/js/Pages/Index.svelte.stub'), + to: 'resources/js/Pages/Index.svelte', + }, + { + from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), + to: 'resources/js/vite-env.d.ts', + }, + ], }) Publisher.publishes({ - package: '@arkstack/inertia', - tag: 'inertia-vue', - entries: [ - { - from: join(root, 'stubs/vue/resources/js/app.ts.stub'), - to: 'resources/js/app.ts', - }, - { - from: join(root, 'stubs/vue/vite.config.ts.stub'), - to: 'vite.config.ts', - }, - { - from: join(root, 'stubs/vue/resources/js/Pages/Index.vue.stub'), - to: 'resources/js/Pages/Index.vue', - }, - { - from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), - to: 'resources/js/vite-env.d.ts', - }, - ], + package: '@arkstack/inertia', + tag: 'inertia-vue', + entries: [ + { + from: join(root, 'stubs/vue/resources/js/app.ts.stub'), + to: 'resources/js/app.ts', + }, + { + from: join(root, 'stubs/vue/vite.config.ts.stub'), + to: 'vite.config.ts', + }, + { + from: join(root, 'stubs/vue/resources/js/Pages/Index.vue.stub'), + to: 'resources/js/Pages/Index.vue', + }, + { + from: join(root, 'stubs/shared/resources/js/vite-env.d.ts.stub'), + to: 'resources/js/vite-env.d.ts', + }, + ], }) Publisher.confirm({ - package: '@arkstack/inertia', - message: 'What front-end framework are you using with Inertia?', - options: [ - { name: 'React', value: 'inertia-react' }, - { name: 'Svelte', value: 'inertia-svelte' }, - { name: 'Vue', value: 'inertia-vue' }, - ], - callback: (tag, stub) => { - const exts: Record = { - 'inertia-react': 'tsx', - 'inertia-svelte': 'ts', - 'inertia-vue': 'ts', - } + package: '@arkstack/inertia', + message: 'What front-end framework are you using with Inertia?', + options: [ + { name: 'React', value: 'inertia-react' }, + { name: 'Svelte', value: 'inertia-svelte' }, + { name: 'Vue', value: 'inertia-vue' }, + ], + callback: (tag, stub) => { + const exts: Record = { + 'inertia-react': 'tsx', + 'inertia-svelte': 'ts', + 'inertia-vue': 'ts', + } - // React needs the Vite Refresh preamble emitted before the entry tags; - // the other frameworks don't, so the placeholder collapses to nothing. - const reactRefresh = tag === 'inertia-react' ? '@viteReactRefresh\n ' : '' + // React needs the Vite Refresh preamble emitted before the entry tags; + // the other frameworks don't, so the placeholder collapses to nothing. + const reactRefresh = tag === 'inertia-react' ? '@viteReactRefresh\n ' : '' - return stub - .replaceAll('{{ext}}', exts[tag] ?? 'ts') - .replaceAll('{{reactRefresh}}', reactRefresh) - } -}) \ No newline at end of file + return stub.replaceAll('{{ext}}', exts[tag] ?? 'ts').replaceAll('{{reactRefresh}}', reactRefresh) + }, +}) diff --git a/packages/inertia/src/ssr-process.ts b/packages/inertia/src/ssr-process.ts index f4f009dc..9b16cfd1 100644 --- a/packages/inertia/src/ssr-process.ts +++ b/packages/inertia/src/ssr-process.ts @@ -10,35 +10,31 @@ export const DEFAULT_SSR_BUNDLE = 'dist-ssr/ssr.js' * Precedence: an explicit `--bundle` option, then `inertia.ssr.bundle` config, * then {@link DEFAULT_SSR_BUNDLE}. Relative paths are resolved from `rootDir`. */ -export const resolveSsrBundle = ( - rootDir: string, - option?: string, - configured?: string, -): string => { - const bundle = option || configured || DEFAULT_SSR_BUNDLE - - return isAbsolute(bundle) ? bundle : join(rootDir, bundle) +export const resolveSsrBundle = (rootDir: string, option?: string, configured?: string): string => { + const bundle = option || configured || DEFAULT_SSR_BUNDLE + + return isAbsolute(bundle) ? bundle : join(rootDir, bundle) } export interface SuperviseOptions { - /** Working directory for the spawned process. */ - cwd?: string - /** Restart the process when it exits unexpectedly (default `true`). */ - restart?: boolean - /** Delay before restarting after a crash, in ms (default `1000`). */ - restartDelayMs?: number - /** Called when the child exits; `willRestart` reflects the restart decision. */ - onExit?: (code: number | null, willRestart: boolean) => void - /** Called when the child fails to spawn. */ - onError?: (error: Error) => void + /** Working directory for the spawned process. */ + cwd?: string + /** Restart the process when it exits unexpectedly (default `true`). */ + restart?: boolean + /** Delay before restarting after a crash, in ms (default `1000`). */ + restartDelayMs?: number + /** Called when the child exits; `willRestart` reflects the restart decision. */ + onExit?: (code: number | null, willRestart: boolean) => void + /** Called when the child fails to spawn. */ + onError?: (error: Error) => void } /** Handle to a supervised process. */ export interface SsrProcessController { - /** Stop the process and prevent further restarts. */ - stop (): void - /** Resolves when the process has stopped (and will not restart). */ - done: Promise + /** Stop the process and prevent further restarts. */ + stop(): void + /** Resolves when the process has stopped (and will not restart). */ + done: Promise } /** @@ -47,55 +43,55 @@ export interface SsrProcessController { * keep the Inertia SSR server alive. */ export const superviseProcess = ( - command: string, - args: string[], - options: SuperviseOptions = {}, + command: string, + args: string[], + options: SuperviseOptions = {}, ): SsrProcessController => { - const { cwd, restart = true, restartDelayMs = 1000 } = options - - let child: ChildProcess | undefined - let timer: NodeJS.Timeout | undefined - let stopping = false - let resolveDone: () => void = () => {} - const done = new Promise((resolve) => { - resolveDone = resolve + const { cwd, restart = true, restartDelayMs = 1000 } = options + + let child: ChildProcess | undefined + let timer: NodeJS.Timeout | undefined + let stopping = false + let resolveDone: () => void = () => {} + const done = new Promise((resolve) => { + resolveDone = resolve + }) + + const start = () => { + child = spawn(command, args, { cwd, stdio: 'inherit' }) + + child.on('error', (error) => { + options.onError?.(error) + resolveDone() }) - const start = () => { - child = spawn(command, args, { cwd, stdio: 'inherit' }) - - child.on('error', (error) => { - options.onError?.(error) - resolveDone() - }) + child.on('exit', (code) => { + const willRestart = !stopping && restart - child.on('exit', (code) => { - const willRestart = !stopping && restart + options.onExit?.(code, willRestart) - options.onExit?.(code, willRestart) + if (!willRestart) { + resolveDone() - if (!willRestart) { - resolveDone() + return + } - return - } - - timer = setTimeout(start, restartDelayMs) - }) - } + timer = setTimeout(start, restartDelayMs) + }) + } - start() + start() - return { - stop () { - stopping = true + return { + stop() { + stopping = true - if (timer) { - clearTimeout(timer) - } + if (timer) { + clearTimeout(timer) + } - child?.kill('SIGTERM') - }, - done, - } + child?.kill('SIGTERM') + }, + done, + } } diff --git a/packages/inertia/src/ssr.ts b/packages/inertia/src/ssr.ts index 9e108e9c..8b1c31a9 100644 --- a/packages/inertia/src/ssr.ts +++ b/packages/inertia/src/ssr.ts @@ -5,10 +5,10 @@ export const DEFAULT_SSR_URL = 'http://127.0.0.1:13714/render' /** The payload returned by an Inertia SSR server for a rendered page. */ export interface SsrResponse { - /** HTML strings to inject into the document `` (title, meta, style). */ - head: string[] - /** The rendered mount markup (JSON `` @@ -28,45 +28,41 @@ const trimTrailingSlash = (value: string) => value.replace(/\/+$/, '') const withTrailingSlash = (value: string) => `${trimTrailingSlash(value)}/` const devTags = (entries: string[], devUrl: string): string => { - const base = trimTrailingSlash(devUrl) + const base = trimTrailingSlash(devUrl) - return [ - scriptTag(`${base}/@vite/client`), - ...entries.map(entry => scriptTag(`${base}/${entry.replace(/^\/+/, '')}`)), - ].join('\n') + return [ + scriptTag(`${base}/@vite/client`), + ...entries.map((entry) => scriptTag(`${base}/${entry.replace(/^\/+/, '')}`)), + ].join('\n') } -const productionTags = ( - entries: string[], - manifestPath: string, - buildDir: string -): string => { - let manifest: Record - - try { - manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) - } catch { - throw new Error(`Vite manifest not found at ${manifestPath}. Run \`vite build\` before serving in production.`) - } +const productionTags = (entries: string[], manifestPath: string, buildDir: string): string => { + let manifest: Record - const base = withTrailingSlash(buildDir) - const tags: string[] = [] + try { + manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + } catch { + throw new Error(`Vite manifest not found at ${manifestPath}. Run \`vite build\` before serving in production.`) + } - for (const entry of entries) { - const chunk = manifest[entry] + const base = withTrailingSlash(buildDir) + const tags: string[] = [] - if (!chunk) { - throw new Error(`Vite manifest (${manifestPath}) has no entry for "${entry}".`) - } + for (const entry of entries) { + const chunk = manifest[entry] - for (const css of chunk.css ?? []) { - tags.push(styleTag(`${base}${css}`)) - } + if (!chunk) { + throw new Error(`Vite manifest (${manifestPath}) has no entry for "${entry}".`) + } - tags.push(isCss(entry) ? styleTag(`${base}${chunk.file}`) : scriptTag(`${base}${chunk.file}`)) + for (const css of chunk.css ?? []) { + tags.push(styleTag(`${base}${css}`)) } - return tags.join('\n') + tags.push(isCss(entry) ? styleTag(`${base}${chunk.file}`) : scriptTag(`${base}${chunk.file}`)) + } + + return tags.join('\n') } /** @@ -76,25 +72,22 @@ const productionTags = ( * at the Vite dev server and includes the `@vite/client`. In production it reads * the build manifest and emits the hashed asset tags (including any CSS a chunk * imports). Backs the `@vite(...)` Edge tag. - * - * @param entries - * @param options - * @returns + * + * @param entries + * @param options + * @returns */ -export const viteTags = ( - entries: string | string[], - options: ViteTagOptions = {} -): string => { - const list = (Array.isArray(entries) ? entries : [entries]).filter(Boolean) - const dev = options.hot ?? (nodeEnv() !== 'prod') - - if (dev) { - return devTags(list, options.devUrl ?? env('VITE_DEV_URL', 'http://localhost:5173')) - } +export const viteTags = (entries: string | string[], options: ViteTagOptions = {}): string => { + const list = (Array.isArray(entries) ? entries : [entries]).filter(Boolean) + const dev = options.hot ?? nodeEnv() !== 'prod' + + if (dev) { + return devTags(list, options.devUrl ?? env('VITE_DEV_URL', 'http://localhost:5173')) + } - const manifestPath = options.manifest ?? join(Arkstack.rootDir(), 'public', 'build', '.vite', 'manifest.json') + const manifestPath = options.manifest ?? join(Arkstack.rootDir(), 'public', 'build', '.vite', 'manifest.json') - return productionTags(list, manifestPath, options.buildDir ?? '/build/') + return productionTags(list, manifestPath, options.buildDir ?? '/build/') } /** @@ -109,23 +102,23 @@ export const viteTags = ( * @returns */ export const viteReactRefresh = (options: ViteTagOptions = {}): string => { - const dev = options.hot ?? (nodeEnv() !== 'prod') - - if (!dev) { - return '' - } - - const base = trimTrailingSlash(options.devUrl ?? env('VITE_DEV_URL', 'http://localhost:5173')) - - return [ - '', - ].join('\n') + const dev = options.hot ?? nodeEnv() !== 'prod' + + if (!dev) { + return '' + } + + const base = trimTrailingSlash(options.devUrl ?? env('VITE_DEV_URL', 'http://localhost:5173')) + + return [ + '', + ].join('\n') } /** @@ -138,43 +131,31 @@ export const viteReactRefresh = (options: ViteTagOptions = {}): string => { * @param factory */ export const registerViteTag = (factory: ViewFactory): void => { - factory.edge.global('__arkViteTags', (entries: string | string[], options?: ViteTagOptions) => - viteTags(entries, options), + factory.edge.global('__arkViteTags', (entries: string | string[], options?: ViteTagOptions) => + viteTags(entries, options), + ) + + factory.edge.global('__arkViteReactRefresh', (options?: ViteTagOptions) => viteReactRefresh(options)) + + factory.tag('vite', false, true, (parser, buffer, token) => { + const expression = `__arkViteTags(${token.properties.jsArg})` + const ast = parser.utils.transformAst( + parser.utils.generateAST(expression, token.loc, token.filename), + token.filename, + parser, ) - factory.edge.global('__arkViteReactRefresh', (options?: ViteTagOptions) => - viteReactRefresh(options), + buffer.outputExpression(parser.utils.stringify(ast), token.filename, token.loc.start.line, false) + }) + + factory.tag('viteReactRefresh', false, false, (parser, buffer, token) => { + const expression = '__arkViteReactRefresh()' + const ast = parser.utils.transformAst( + parser.utils.generateAST(expression, token.loc, token.filename), + token.filename, + parser, ) - factory.tag('vite', false, true, (parser, buffer, token) => { - const expression = `__arkViteTags(${token.properties.jsArg})` - const ast = parser.utils.transformAst( - parser.utils.generateAST(expression, token.loc, token.filename), - token.filename, - parser, - ) - - buffer.outputExpression( - parser.utils.stringify(ast), - token.filename, - token.loc.start.line, - false - ) - }) - - factory.tag('viteReactRefresh', false, false, (parser, buffer, token) => { - const expression = '__arkViteReactRefresh()' - const ast = parser.utils.transformAst( - parser.utils.generateAST(expression, token.loc, token.filename), - token.filename, - parser, - ) - - buffer.outputExpression( - parser.utils.stringify(ast), - token.filename, - token.loc.start.line, - false - ) - }) + buffer.outputExpression(parser.utils.stringify(ast), token.filename, token.loc.start.line, false) + }) } diff --git a/packages/view/tests/make-view-command.test.ts b/packages/view/tests/make-view-command.test.ts index 4c31021b..e41cfb13 100644 --- a/packages/view/tests/make-view-command.test.ts +++ b/packages/view/tests/make-view-command.test.ts @@ -7,42 +7,42 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' describe('MakeViewCommand', () => { - let cwd: string | undefined - - afterEach(async () => { - if (cwd) { - await rm(cwd, { recursive: true, force: true }) - cwd = undefined - } - }) - - it('creates dot-notated Edge view files in resources/views', async () => { - cwd = await mkdtemp(join(tmpdir(), 'arkstack-make-view-')) - Arkstack.setRootDir(cwd) - const previous = Arkstack.rootDir() - process.chdir(cwd) - - try { - const command = new MakeViewCommand({} as never, {} as never) - const messages: string[] = [] - const path = command.path('admin.users.index') - const root = (await realpath(cwd)).replace('/private', '') - - Object.assign(command, { - argument: () => 'admin.users.index', - option: () => false, - success: (message: string) => messages.push(message), - error: (message: string) => messages.push(message), - }) - - await command.handle() - - expect(path).toBe(join(root, 'src', 'resources', 'views', 'admin', 'users', 'index.edge')) - await expect(readFile(path, 'utf8')).resolves.toContain('index view') - expect(command.stub('admin.users.index')).toContain('index view') - expect(messages[0]).toContain('View admin.users.index created successfully') - } finally { - process.chdir(previous) - } - }) + let cwd: string | undefined + + afterEach(async () => { + if (cwd) { + await rm(cwd, { recursive: true, force: true }) + cwd = undefined + } + }) + + it('creates dot-notated Edge view files in resources/views', async () => { + cwd = await mkdtemp(join(tmpdir(), 'arkstack-make-view-')) + Arkstack.setRootDir(cwd) + const previous = Arkstack.rootDir() + process.chdir(cwd) + + try { + const command = new MakeViewCommand({} as never, {} as never) + const messages: string[] = [] + const path = command.path('admin.users.index') + const root = (await realpath(cwd)).replace('/private', '') + + Object.assign(command, { + argument: () => 'admin.users.index', + option: () => false, + success: (message: string) => messages.push(message), + error: (message: string) => messages.push(message), + }) + + await command.handle() + + expect(path).toBe(join(root, 'src', 'resources', 'views', 'admin', 'users', 'index.edge')) + await expect(readFile(path, 'utf8')).resolves.toContain('index view') + expect(command.stub('admin.users.index')).toContain('index view') + expect(messages[0]).toContain('View admin.users.index created successfully') + } finally { + process.chdir(previous) + } + }) }) diff --git a/packages/view/tests/view.test.ts b/packages/view/tests/view.test.ts index a2f7bcbe..665b9dfc 100644 --- a/packages/view/tests/view.test.ts +++ b/packages/view/tests/view.test.ts @@ -13,230 +13,233 @@ const test = parseFloat(process.versions.node) > 22.99 ? it : it.skip View.boot() const clearTestSession = () => { - const session = globalThis.session?.() + const session = globalThis.session?.() - if (session && typeof session === 'object' && 'clear' in session && typeof session.clear === 'function') { - session.clear() - } + if (session && typeof session === 'object' && 'clear' in session && typeof session.clear === 'function') { + session.clear() + } - delete (globalThis as any).session + delete (globalThis as any).session } describe('View', () => { - beforeEach(async () => { - viewsPath = await mkdtemp(join(tmpdir(), 'arkstack-view-')) - View.configure({ viewsPath }) - }) + beforeEach(async () => { + viewsPath = await mkdtemp(join(tmpdir(), 'arkstack-view-')) + View.configure({ viewsPath }) + }) - afterEach(async () => { - clearTestSession() - clearViewData() - await rm(viewsPath, { recursive: true, force: true }) - }) + afterEach(async () => { + clearTestSession() + clearViewData() + await rm(viewsPath, { recursive: true, force: true }) + }) - it('renders views from the imported helper and static View API', async () => { - await writeFile(join(viewsPath, 'welcome.edge'), 'Hello {{ name }} from {{ app }}') + it('renders views from the imported helper and static View API', async () => { + await writeFile(join(viewsPath, 'welcome.edge'), 'Hello {{ name }} from {{ app }}') - View.share({ app: 'Arkstack' }) + View.share({ app: 'Arkstack' }) - await expect(view('welcome', { name: 'Ada' })).resolves.toBe('Hello Ada from Arkstack') - await expect(View.make('welcome', { name: 'Grace' }).render()).resolves.toBe('Hello Grace from Arkstack') - }) + await expect(view('welcome', { name: 'Ada' })).resolves.toBe('Hello Ada from Arkstack') + await expect(View.make('welcome', { name: 'Grace' }).render()).resolves.toBe('Hello Grace from Arkstack') + }) - it('returns the factory from view() and supports global share data', async () => { - await writeFile(join(viewsPath, 'home.edge'), '{{ greeting }}, {{ name }}') + it('returns the factory from view() and supports global share data', async () => { + await writeFile(join(viewsPath, 'home.edge'), '{{ greeting }}, {{ name }}') - view().share({ greeting: 'Hello' }).share('name', 'Ada') + view().share({ greeting: 'Hello' }).share('name', 'Ada') - await expect(view('home')).resolves.toBe('Hello, Ada') - expect(globalThis.view()).toBe(view()) - }) + await expect(view('home')).resolves.toBe('Hello, Ada') + expect(globalThis.view()).toBe(view()) + }) - it('supports local data through view instance with calls', async () => { - await writeFile(join(viewsPath, 'local.edge'), '{{ greeting }}, {{ name }}') + it('supports local data through view instance with calls', async () => { + await writeFile(join(viewsPath, 'local.edge'), '{{ greeting }}, {{ name }}') - await expect(view('local').with({ greeting: 'Hello' }).with('name', 'Ada')).resolves.toBe('Hello, Ada') - }) + await expect(view('local').with({ greeting: 'Hello' }).with('name', 'Ada')).resolves.toBe('Hello, Ada') + }) - it('checks existence and renders the first existing view', async () => { - await writeFile(join(viewsPath, 'fallback.edge'), 'Fallback {{ name }}') + it('checks existence and renders the first existing view', async () => { + await writeFile(join(viewsPath, 'fallback.edge'), 'Fallback {{ name }}') - expect(View.exists('missing')).toBe(false) - expect(View.exists('fallback')).toBe(true) + expect(View.exists('missing')).toBe(false) + expect(View.exists('fallback')).toBe(true) - await expect(View.first(['missing', 'fallback'], { name: 'Ada' })).resolves.toBe('Fallback Ada') - }) + await expect(View.first(['missing', 'fallback'], { name: 'Ada' })).resolves.toBe('Fallback Ada') + }) - it('throws when none of the fallback views exist', () => { - expect(() => View.first(['missing', 'also-missing'])).toThrow('None of the given views exist: missing, also-missing') + it('throws when none of the fallback views exist', () => { + expect(() => View.first(['missing', 'also-missing'])).toThrow( + 'None of the given views exist: missing, also-missing', + ) + }) + + it('renders raw views synchronously with composer data', () => { + View.raw('inline', '{{ greeting }}, {{ name }}') + View.composer('inline', (renderedView) => { + renderedView.with('greeting', 'Hello') }) - it('renders raw views synchronously with composer data', () => { - View.raw('inline', '{{ greeting }}, {{ name }}') - View.composer('inline', renderedView => { - renderedView.with('greeting', 'Hello') - }) + const renderedView = View.make('inline').with('name', 'Ada') + + expect(renderedView.renderSync()).toBe('Hello, Ada') + }) - const renderedView = View.make('inline').with('name', 'Ada') + it('flushes shared data and composers independently', async () => { + await writeFile(join(viewsPath, 'flush.edge'), '{{ greeting || "No greeting" }} {{ name || "No name" }}') - expect(renderedView.renderSync()).toBe('Hello, Ada') + View.share('greeting', 'Hello') + View.composer('flush', (renderedView) => { + renderedView.with('name', 'Ada') }) - it('flushes shared data and composers independently', async () => { - await writeFile(join(viewsPath, 'flush.edge'), '{{ greeting || "No greeting" }} {{ name || "No name" }}') + await expect(View.make('flush')).resolves.toBe('Hello Ada') - View.share('greeting', 'Hello') - View.composer('flush', renderedView => { - renderedView.with('name', 'Ada') - }) + View.factoryInstance().flushShared() + await expect(View.make('flush')).resolves.toBe('No greeting Ada') - await expect(View.make('flush')).resolves.toBe('Hello Ada') + View.factoryInstance().flushComposers() + await expect(View.make('flush')).resolves.toBe('No greeting No name') + }) - View.factoryInstance().flushShared() - await expect(View.make('flush')).resolves.toBe('No greeting Ada') + it('provides a safe error bag to views', async () => { + await writeFile(join(viewsPath, 'form.edge'), '{{ errors.has("email") ? errors.first("email") : "No errors" }}') - View.factoryInstance().flushComposers() - await expect(View.make('flush')).resolves.toBe('No greeting No name') - }) + await expect(view('form')).resolves.toBe('No errors') + await expect(view('form', { errors: { email: ['Email is required'] } })).resolves.toBe('Email is required') + }) - it('provides a safe error bag to views', async () => { - await writeFile(join(viewsPath, 'form.edge'), '{{ errors.has("email") ? errors.first("email") : "No errors" }}') + it('falls back to the active HTTP session errors for views', async () => { + await writeFile(join(viewsPath, 'session-errors.edge'), '{{ errors.first("email") }}') - await expect(view('form')).resolves.toBe('No errors') - await expect(view('form', { errors: { email: ['Email is required'] } })).resolves.toBe('Email is required') + new Session({ + errors: { + email: ['Email is required'], + }, }) - it('falls back to the active HTTP session errors for views', async () => { - await writeFile(join(viewsPath, 'session-errors.edge'), '{{ errors.first("email") }}') - - new Session({ - errors: { - email: ['Email is required'], - }, - }) + await expect(view('session-errors')).resolves.toBe('Email is required') + }) - await expect(view('session-errors')).resolves.toBe('Email is required') - }) + it('uses per-request view data from the Clear Router view plugin', async () => { + await writeFile(join(viewsPath, 'request.edge'), '{{ session.get("intended") }}:{{ errors.first("email") }}') - it('uses per-request view data from the Clear Router view plugin', async () => { - await writeFile(join(viewsPath, 'request.edge'), '{{ session.get("intended") }}:{{ errors.first("email") }}') + let resolver: (context: any) => void | Promise = () => undefined + clearRouterViewPlugin.setup({ + useHttpContext(callback: any) { + resolver = callback + }, + } as never) - let resolver: (context: any) => void | Promise = () => undefined - clearRouterViewPlugin.setup({ - useHttpContext (callback: any) { - resolver = callback + await resolver({ + ctx: { + res: { + locals: { + session: { + get: (key: string) => (key === 'intended' ? '/dashboard' : undefined), }, - } as never) - - await resolver({ - ctx: { - res: { - locals: { - session: { - get: (key: string) => key === 'intended' ? '/dashboard' : undefined, - }, - errors: { - first: (field: string) => field === 'email' ? 'Email is required' : '', - get: () => ['Email is required'], - has: () => true, - all: () => ['Email is required'], - }, - }, - }, + errors: { + first: (field: string) => (field === 'email' ? 'Email is required' : ''), + get: () => ['Email is required'], + has: () => true, + all: () => ['Email is required'], }, - }) - - await expect(view('request')).resolves.toBe('/dashboard:Email is required') + }, + }, + }, }) - test('scopes explicit view context data to a render callback', async () => { - await writeFile(join(viewsPath, 'scoped.edge'), '{{ errors.first("email") || "empty" }}') + await expect(view('request')).resolves.toBe('/dashboard:Email is required') + }) - await expect(runWithViewData( - { errors: { email: ['Scoped error'] } }, - () => view('scoped'), - )).resolves.toBe('Scoped error') - await expect(view('scoped')).resolves.toBe('empty') - }) + test('scopes explicit view context data to a render callback', async () => { + await writeFile(join(viewsPath, 'scoped.edge'), '{{ errors.first("email") || "empty" }}') - it('runs wildcard and named view composers before rendering', async () => { - await writeFile(join(viewsPath, 'profile.edge'), '{{ title }}: {{ name }}{{ suffix }}') + await expect(runWithViewData({ errors: { email: ['Scoped error'] } }, () => view('scoped'))).resolves.toBe( + 'Scoped error', + ) + await expect(view('scoped')).resolves.toBe('empty') + }) - class ProfileComposer { - compose (renderedView: ViewInstance) { - renderedView.with('name', 'Ada') - } - } + it('runs wildcard and named view composers before rendering', async () => { + await writeFile(join(viewsPath, 'profile.edge'), '{{ title }}: {{ name }}{{ suffix }}') - class ProfileSuffixComposer { - compose (renderedView: ViewInstance) { - renderedView.with('suffix', '!') - } - } - - View.composer('*', renderedView => { - renderedView.with({ title: 'User' }) - }) + class ProfileComposer { + compose(renderedView: ViewInstance) { + renderedView.with('name', 'Ada') + } + } - View.composer('profile', ProfileComposer) - View.composer('profile', new ProfileSuffixComposer()) + class ProfileSuffixComposer { + compose(renderedView: ViewInstance) { + renderedView.with('suffix', '!') + } + } - await expect(View.make('profile')).resolves.toBe('User: Ada!') + View.composer('*', (renderedView) => { + renderedView.with({ title: 'User' }) }) - it('can create isolated factories for alternate view roots', async () => { - const factory = new ViewFactory({ viewsPath }) + View.composer('profile', ProfileComposer) + View.composer('profile', new ProfileSuffixComposer()) - await writeFile(join(viewsPath, 'plain.edge'), '{{ message }}') + await expect(View.make('profile')).resolves.toBe('User: Ada!') + }) - await expect(factory.make('plain').with({ message: 'Hi' })).resolves.toBe('Hi') - }) + it('can create isolated factories for alternate view roots', async () => { + const factory = new ViewFactory({ viewsPath }) - it('renders unscoped package views with tilde notation', async () => { - const root = await mkdtemp(join(tmpdir(), 'arkstack-package-view-')) - Arkstack.setRootDir(root) - const previous = Arkstack.rootDir() + await writeFile(join(viewsPath, 'plain.edge'), '{{ message }}') - process.chdir(root) + await expect(factory.make('plain').with({ message: 'Hi' })).resolves.toBe('Hi') + }) - try { - const packageViews = join(root, 'node_modules', 'billing-kit', 'resources', 'views') + it('renders unscoped package views with tilde notation', async () => { + const root = await mkdtemp(join(tmpdir(), 'arkstack-package-view-')) + Arkstack.setRootDir(root) + const previous = Arkstack.rootDir() - await mkdir(packageViews, { recursive: true }) - await writeFile(join(packageViews, 'mail.edge'), 'Invoice {{ number }}') + process.chdir(root) - const factory = new ViewFactory({ viewsPath }) + try { + const packageViews = join(root, 'node_modules', 'billing-kit', 'resources', 'views') - expect(factory.exists('~billing-kit.mail')).toBe(true) - await expect(factory.make('~billing-kit.mail', { number: 'A-100' })).resolves.toBe('Invoice A-100') - } finally { - process.chdir(previous) - await rm(root, { recursive: true, force: true }) - } - }) + await mkdir(packageViews, { recursive: true }) + await writeFile(join(packageViews, 'mail.edge'), 'Invoice {{ number }}') - it('renders scoped package views with tilde notation', async () => { - const root = await mkdtemp(join(tmpdir(), 'arkstack-scoped-package-view-')) - Arkstack.setRootDir(root) - const previous = Arkstack.rootDir() - - process.chdir(root) - - try { - const packageViews = join(root, 'node_modules', '@toneflix', 'mail-kit', 'resources', 'views') - await mkdir(packageViews, { recursive: true }) - await writeFile(join(packageViews, 'mail.edge'), 'Welcome {{ name }} from {{ source }}') - - const factory = new ViewFactory({ viewsPath }) - factory.composer('~toneflix/mail-kit.mail', renderedView => { - renderedView.with('source', renderedView.name) - }) - - expect(factory.exists('~toneflix/mail-kit.mail')).toBe(true) - await expect(factory.make('~toneflix/mail-kit.mail', { name: 'Ada' })).resolves.toBe('Welcome Ada from ~toneflix/mail-kit.mail') - } finally { - process.chdir(previous) - await rm(root, { recursive: true, force: true }) - } - }) + const factory = new ViewFactory({ viewsPath }) + + expect(factory.exists('~billing-kit.mail')).toBe(true) + await expect(factory.make('~billing-kit.mail', { number: 'A-100' })).resolves.toBe('Invoice A-100') + } finally { + process.chdir(previous) + await rm(root, { recursive: true, force: true }) + } + }) + + it('renders scoped package views with tilde notation', async () => { + const root = await mkdtemp(join(tmpdir(), 'arkstack-scoped-package-view-')) + Arkstack.setRootDir(root) + const previous = Arkstack.rootDir() + + process.chdir(root) + + try { + const packageViews = join(root, 'node_modules', '@toneflix', 'mail-kit', 'resources', 'views') + await mkdir(packageViews, { recursive: true }) + await writeFile(join(packageViews, 'mail.edge'), 'Welcome {{ name }} from {{ source }}') + + const factory = new ViewFactory({ viewsPath }) + factory.composer('~toneflix/mail-kit.mail', (renderedView) => { + renderedView.with('source', renderedView.name) + }) + + expect(factory.exists('~toneflix/mail-kit.mail')).toBe(true) + await expect(factory.make('~toneflix/mail-kit.mail', { name: 'Ada' })).resolves.toBe( + 'Welcome Ada from ~toneflix/mail-kit.mail', + ) + } finally { + process.chdir(previous) + await rm(root, { recursive: true, force: true }) + } + }) }) diff --git a/packages/view/tests/vite.test.ts b/packages/view/tests/vite.test.ts index 6ef13ce8..6c9ee3c0 100644 --- a/packages/view/tests/vite.test.ts +++ b/packages/view/tests/vite.test.ts @@ -8,109 +8,115 @@ import { tmpdir } from 'node:os' const tmpDirs: string[] = [] const writeManifest = (manifest: Record): string => { - const dir = mkdtempSync(join(tmpdir(), 'ark-vite-')) - tmpDirs.push(dir) - const path = join(dir, 'manifest.json') - writeFileSync(path, JSON.stringify(manifest)) + const dir = mkdtempSync(join(tmpdir(), 'ark-vite-')) + tmpDirs.push(dir) + const path = join(dir, 'manifest.json') + writeFileSync(path, JSON.stringify(manifest)) - return path + return path } afterEach(() => { - while (tmpDirs.length) { - rmSync(tmpDirs.pop()!, { recursive: true, force: true }) - } + while (tmpDirs.length) { + rmSync(tmpDirs.pop()!, { recursive: true, force: true }) + } }) describe('viteTags (development)', () => { - test('emits the dev client and module scripts', () => { - const html = viteTags('resources/js/app.ts', { hot: true }) + test('emits the dev client and module scripts', () => { + const html = viteTags('resources/js/app.ts', { hot: true }) - expect(html).toContain('') - expect(html).toContain('') - }) - - test('honours a custom dev URL and multiple entries', () => { - const html = viteTags(['resources/css/app.css', 'resources/js/app.ts'], { - hot: true, - devUrl: 'http://127.0.0.1:3001/', - }) + expect(html).toContain('') + expect(html).toContain('') + }) - expect(html).toContain('http://127.0.0.1:3001/@vite/client') - expect(html).toContain('http://127.0.0.1:3001/resources/css/app.css') - expect(html).toContain('http://127.0.0.1:3001/resources/js/app.ts') + test('honours a custom dev URL and multiple entries', () => { + const html = viteTags(['resources/css/app.css', 'resources/js/app.ts'], { + hot: true, + devUrl: 'http://127.0.0.1:3001/', }) + + expect(html).toContain('http://127.0.0.1:3001/@vite/client') + expect(html).toContain('http://127.0.0.1:3001/resources/css/app.css') + expect(html).toContain('http://127.0.0.1:3001/resources/js/app.ts') + }) }) describe('viteTags (production)', () => { - test('resolves hashed assets and imported css from the manifest', () => { - const manifest = writeManifest({ - 'resources/js/app.ts': { file: 'assets/app-abc123.js', css: ['assets/app-def456.css'] }, - 'resources/css/app.css': { file: 'assets/style-xyz789.css' }, - }) - - const html = viteTags(['resources/css/app.css', 'resources/js/app.ts'], { - hot: false, - manifest, - buildDir: '/build/', - }) - - expect(html).toContain('') - expect(html).toContain('') - expect(html).toContain('') - expect(html).not.toContain('@vite/client') + test('resolves hashed assets and imported css from the manifest', () => { + const manifest = writeManifest({ + 'resources/js/app.ts': { + file: 'assets/app-abc123.js', + css: ['assets/app-def456.css'], + }, + 'resources/css/app.css': { file: 'assets/style-xyz789.css' }, }) - test('throws when the manifest is missing', () => { - expect(() => viteTags('app.ts', { hot: false, manifest: '/no/such/manifest.json' })) - .toThrow(/manifest not found/i) + const html = viteTags(['resources/css/app.css', 'resources/js/app.ts'], { + hot: false, + manifest, + buildDir: '/build/', }) - test('throws when an entry is absent from the manifest', () => { - const manifest = writeManifest({ 'resources/js/app.ts': { file: 'assets/app.js' } }) + expect(html).toContain('') + expect(html).toContain('') + expect(html).toContain('') + expect(html).not.toContain('@vite/client') + }) - expect(() => viteTags('resources/js/missing.ts', { hot: false, manifest })) - .toThrow(/no entry for/i) + test('throws when the manifest is missing', () => { + expect(() => viteTags('app.ts', { hot: false, manifest: '/no/such/manifest.json' })).toThrow(/manifest not found/i) + }) + + test('throws when an entry is absent from the manifest', () => { + const manifest = writeManifest({ + 'resources/js/app.ts': { file: 'assets/app.js' }, }) + + expect(() => viteTags('resources/js/missing.ts', { hot: false, manifest })).toThrow(/no entry for/i) + }) }) describe('viteReactRefresh', () => { - test('emits the React Refresh preamble in development', () => { - const html = viteReactRefresh({ hot: true }) + test('emits the React Refresh preamble in development', () => { + const html = viteReactRefresh({ hot: true }) - expect(html).toContain('import RefreshRuntime from \'http://localhost:5173/@react-refresh\'') - expect(html).toContain('window.__vite_plugin_react_preamble_installed__ = true') - }) + expect(html).toContain("import RefreshRuntime from 'http://localhost:5173/@react-refresh'") + expect(html).toContain('window.__vite_plugin_react_preamble_installed__ = true') + }) - test('honours a custom dev URL', () => { - const html = viteReactRefresh({ hot: true, devUrl: 'http://127.0.0.1:3001/' }) - - expect(html).toContain('import RefreshRuntime from \'http://127.0.0.1:3001/@react-refresh\'') + test('honours a custom dev URL', () => { + const html = viteReactRefresh({ + hot: true, + devUrl: 'http://127.0.0.1:3001/', }) - test('emits nothing in production', () => { - expect(viteReactRefresh({ hot: false })).toBe('') - }) + expect(html).toContain("import RefreshRuntime from 'http://127.0.0.1:3001/@react-refresh'") + }) + + test('emits nothing in production', () => { + expect(viteReactRefresh({ hot: false })).toBe('') + }) }) describe('@vite tag', () => { - test('renders dev tags through the Edge tag', async () => { - const factory = new ViewFactory() - factory.raw('with-vite', '@vite("resources/js/app.ts")') + test('renders dev tags through the Edge tag', async () => { + const factory = new ViewFactory() + factory.raw('with-vite', '@vite("resources/js/app.ts")') - const html = await factory.make('with-vite').render() + const html = await factory.make('with-vite').render() - expect(html).toContain('http://localhost:5173/@vite/client') - expect(html).toContain('http://localhost:5173/resources/js/app.ts') - }) + expect(html).toContain('http://localhost:5173/@vite/client') + expect(html).toContain('http://localhost:5173/resources/js/app.ts') + }) - test('renders the React Refresh preamble through the @viteReactRefresh tag', async () => { - const factory = new ViewFactory() - factory.raw('with-refresh', '@viteReactRefresh') + test('renders the React Refresh preamble through the @viteReactRefresh tag', async () => { + const factory = new ViewFactory() + factory.raw('with-refresh', '@viteReactRefresh') - const html = await factory.make('with-refresh').render() + const html = await factory.make('with-refresh').render() - expect(html).toContain('import RefreshRuntime from \'http://localhost:5173/@react-refresh\'') - expect(html).toContain('window.__vite_plugin_react_preamble_installed__ = true') - }) + expect(html).toContain("import RefreshRuntime from 'http://localhost:5173/@react-refresh'") + expect(html).toContain('window.__vite_plugin_react_preamble_installed__ = true') + }) }) diff --git a/packages/view/tsdown.config.ts b/packages/view/tsdown.config.ts index ab9908ef..042c65b3 100644 --- a/packages/view/tsdown.config.ts +++ b/packages/view/tsdown.config.ts @@ -2,18 +2,19 @@ import { baseConfig } from '../../tsdown.config' import { defineConfig } from 'tsdown' export default defineConfig([ - { - ...baseConfig, - entry: ['src/index.ts', 'src/setup.ts'], - format: 'esm', - shims: false, - outDir: 'dist', - }, { - ...baseConfig, - entry: ['src/commands/*.ts'], - format: 'esm', - shims: false, - dts: false, - outDir: 'dist/commands', - } + { + ...baseConfig, + entry: ['src/index.ts', 'src/setup.ts'], + format: 'esm', + shims: false, + outDir: 'dist', + }, + { + ...baseConfig, + entry: ['src/commands/*.ts'], + format: 'esm', + shims: false, + dts: false, + outDir: 'dist/commands', + }, ]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 871f666b..f34eeb27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: eslint: specifier: ^10.3.0 version: 10.3.0(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.3.0(jiti@2.7.0)) express: specifier: 'catalog:' version: 5.2.1 @@ -168,6 +171,12 @@ importers: pg: specifier: 'catalog:' version: 8.22.0 + prettier: + specifier: ^3.8.4 + version: 3.9.10 + prettier-plugin-brace-style: + specifier: ^0.10.1 + version: 0.10.3(prettier@3.9.10) react: specifier: ^19.2.3 version: 19.2.7 @@ -4299,6 +4308,12 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5692,11 +5707,32 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-brace-style@0.10.3: + resolution: {integrity: sha512-PZbKmdbZSBRzjY30Or+xBKFimVQhqHO6hiAEqg1iJ1kZMb/CHC4LxWhc2CJBhGnx/zkc/HINXZgWOVzyq9gIUQ==} + engines: {node: '>=18'} + peerDependencies: + '@prettier/plugin-oxc': '*' + prettier: ^3 + prettier-plugin-astro: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@prettier/plugin-oxc': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-svelte: + optional: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} + engines: {node: '>=14'} + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -6736,6 +6772,9 @@ packages: yuku-parser@0.8.3: resolution: {integrity: sha512-KPQcpF9aj77ywlJBIkQWCQ9DObdxnCA8AJdUOmA5CZZx042Xt4+dvbQmPJfWxF3E+KG5dVAZ2fBKuDJ8VsKWgA==} + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -10194,6 +10233,10 @@ snapshots: escape-string-regexp@5.0.0: {} + eslint-config-prettier@10.1.8(eslint@10.3.0(jiti@2.7.0)): + dependencies: + eslint: 10.3.0(jiti@2.7.0) + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -11884,8 +11927,15 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-brace-style@0.10.3(prettier@3.9.10): + dependencies: + prettier: 3.9.10 + zod: 3.22.4 + prettier@2.8.8: {} + prettier@3.9.10: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -12962,4 +13012,6 @@ snapshots: '@yuku-parser/binding-win32-arm64': 0.8.3 '@yuku-parser/binding-win32-x64': 0.8.3 + zod@3.22.4: {} + zwitch@2.0.4: {} diff --git a/templates/express-inertia/eslint.config.js b/templates/express-inertia/eslint.config.js index cfa6a459..b7f24597 100644 --- a/templates/express-inertia/eslint.config.js +++ b/templates/express-inertia/eslint.config.js @@ -1,72 +1,74 @@ import { defineConfig } from 'eslint/config' +import eslintConfigPrettier from 'eslint-config-prettier' import { globalIgnores } from 'eslint/config' import js from '@eslint/js' import markdown from '@eslint/markdown' import tseslint from 'typescript-eslint' export default defineConfig( - { - languageOptions: { - globals: { - console: 'readonly', - process: 'readonly', - }, - parserOptions: { - tsconfigRootDir: process.cwd() - } - }, + { + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + }, + parserOptions: { + tsconfigRootDir: process.cwd(), + }, }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ['docs/**/*.md', 'README.md'], - plugins: { - markdown, - }, - extends: ['markdown/recommended'], - rules: { - 'no-irregular-whitespace': 'off', - 'markdown/no-missing-label-refs': 'off', - } + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['docs/**/*.md', 'README.md'], + plugins: { + markdown, + }, + extends: ['markdown/recommended'], + rules: { + 'no-irregular-whitespace': 'off', + 'markdown/no-missing-label-refs': 'off', }, - [ - globalIgnores([ - 'docs/.vitepress/**', - 'bin/**', - 'dist/**', - 'build/**', - '.arkormx/**', - '.arkstack/**', - 'node_modules/**', - ]) - ], - { - rules: { - 'brace-style': [ - 'error', - '1tbs', - { 'allowSingleLine': false }, - ], - 'no-console': 'off', - 'no-thenable': 'off', - // 'no-ternary': 'error', - 'newline-before-return': 'error', - 'semi': ['error', 'never'], - 'quotes': ['error', 'single'], - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', { - 'argsIgnorePattern': '^_|_', - 'vars': 'all', - 'args': 'after-used', - 'ignoreRestSiblings': false, - 'varsIgnorePattern': '^I[A-Z]|^_', - } - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/triple-slash-reference': ['error', { - 'path': 'always' - }] - } + }, + [ + globalIgnores([ + 'docs/.vitepress/**', + 'bin/**', + 'dist/**', + 'build/**', + '.arkormx/**', + '.arkstack/**', + 'node_modules/**', + ]), + ], + { + rules: { + 'brace-style': ['error', '1tbs', { allowSingleLine: false }], + 'no-console': 'off', + 'no-thenable': 'off', + // 'no-ternary': 'error', + 'newline-before-return': 'error', + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_|_', + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + varsIgnorePattern: '^I[A-Z]|^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + }, + ], }, -) \ No newline at end of file + }, + eslintConfigPrettier, +) diff --git a/templates/express-inertia/public/index.json b/templates/express-inertia/public/index.json index 3c1e30e8..9d163840 100644 --- a/templates/express-inertia/public/index.json +++ b/templates/express-inertia/public/index.json @@ -18,4 +18,4 @@ "p": "See the world through our own eyes." } } -} \ No newline at end of file +} diff --git a/templates/express-inertia/resources/js/Pages/Index.tsx b/templates/express-inertia/resources/js/Pages/Index.tsx index 336edff0..de3d1f75 100644 --- a/templates/express-inertia/resources/js/Pages/Index.tsx +++ b/templates/express-inertia/resources/js/Pages/Index.tsx @@ -3,9 +3,9 @@ export default function Index({ message, title: title_, }: { - appName: string; - title: string; - message: string; + appName: string + title: string + message: string }) { const root = document.documentElement const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches @@ -16,10 +16,7 @@ export default function Index({
- Arkstack logo + Arkstack logo

@@ -39,41 +36,25 @@ export default function Index({
- +
Fast by default
-
- Zero-bloat core. Ships only what you need. -
+
Zero-bloat core. Ships only what you need.
- +
Adapters
-
- Express, Fastify, Hono, Koa — swap runtimes freely. -
+
Express, Fastify, Hono, Koa — swap runtimes freely.
- +
@@ -88,13 +69,11 @@ export default function Index({ // src/routes/api.ts
- import {'{ Router }'}{' '} - from{' '} + import {'{ Router }'} from{' '} '@arkstack/driver-express'

- Router.get( - '/', () => {'{'} + Router.get('/', () => {'{'}
  return {'{'} status:{' '} 'ok' {'}'} @@ -105,12 +84,10 @@ export default function Index({

Docs  ·  - GitHub{' '} -  ·  + GitHub  ·  Discord
- Arkstack © {new Date().getFullYear()} — By Toneflix - Technologies + Arkstack © {new Date().getFullYear()} — By Toneflix Technologies

) diff --git a/templates/express-inertia/resources/js/app.tsx b/templates/express-inertia/resources/js/app.tsx index 5424931a..d7ea54bf 100644 --- a/templates/express-inertia/resources/js/app.tsx +++ b/templates/express-inertia/resources/js/app.tsx @@ -4,10 +4,7 @@ import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob<{ default: ComponentType }>( - './Pages/**/*.tsx', - { eager: true }, - ) + const pages = import.meta.glob<{ default: ComponentType }>('./Pages/**/*.tsx', { eager: true }) return pages[`./Pages/${name}.tsx`] }, diff --git a/templates/express-inertia/src/app/http/controllers/BaseController.ts b/templates/express-inertia/src/app/http/controllers/BaseController.ts index 43e864dd..8355ad40 100644 --- a/templates/express-inertia/src/app/http/controllers/BaseController.ts +++ b/templates/express-inertia/src/app/http/controllers/BaseController.ts @@ -4,18 +4,21 @@ import { Controller } from 'clear-router' import { HttpContext } from 'clear-router/types/express' export class BaseController extends Controller { - async validate, R extends RulesForData> ( + async validate, R extends RulesForData>( rules: R, - camelizeKeys = false + camelizeKeys = false, ) { const data = await Validator.make(this.body, rules).validate() if (camelizeKeys) { - return Object.keys(data).reduce((result, key) => { - result[str(key).camel().toString()] = data[key as never] + return Object.keys(data).reduce( + (result, key) => { + result[str(key).camel().toString()] = data[key as never] - return result - }, {} as Record) as { [K in keyof R]: any } + return result + }, + {} as Record, + ) as { [K in keyof R]: any } } return data as { [K in keyof R]: any } diff --git a/templates/express-inertia/src/app/http/controllers/UserController.ts b/templates/express-inertia/src/app/http/controllers/UserController.ts index 352af7ac..88aec5bf 100644 --- a/templates/express-inertia/src/app/http/controllers/UserController.ts +++ b/templates/express-inertia/src/app/http/controllers/UserController.ts @@ -81,7 +81,7 @@ export default class UserController extends BaseController { } /** - * Delete a specific resource + * Delete a specific resource */ async destroy() { return new Resource({}) diff --git a/templates/express-inertia/src/app/http/resources/UserCollection.ts b/templates/express-inertia/src/app/http/resources/UserCollection.ts index a960e666..eedc2686 100644 --- a/templates/express-inertia/src/app/http/resources/UserCollection.ts +++ b/templates/express-inertia/src/app/http/resources/UserCollection.ts @@ -10,7 +10,7 @@ export class UserCollection extends ResourceCollection { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/express-inertia/src/app/http/resources/UserResource.ts b/templates/express-inertia/src/app/http/resources/UserResource.ts index f4de6048..e5247c06 100644 --- a/templates/express-inertia/src/app/http/resources/UserResource.ts +++ b/templates/express-inertia/src/app/http/resources/UserResource.ts @@ -8,7 +8,7 @@ export class UserResource extends Resource { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/express-inertia/src/app/models/PasswordReset.ts b/templates/express-inertia/src/app/models/PasswordReset.ts index d7cf2fb7..a83961a1 100644 --- a/templates/express-inertia/src/app/models/PasswordReset.ts +++ b/templates/express-inertia/src/app/models/PasswordReset.ts @@ -1,29 +1,31 @@ import { Model } from 'arkormx' export class PasswordReset extends Model { - declare id: string - declare email: string | null - declare phone: string | null - declare token: string - declare createdAt: Date - declare updatedAt: Date + declare id: string + declare email: string | null + declare phone: string | null + declare token: string + declare createdAt: Date + declare updatedAt: Date - static fromToken (token: string) { - if (token.length > 6) { - token = Buffer.from(token, 'base64url').toString().split('-')[0] - } - - return this.query().where({ token }).firstOrFail() + static fromToken(token: string) { + if (token.length > 6) { + token = Buffer.from(token, 'base64url').toString().split('-')[0] } - /** - * get the password reset link - */ - link () { - const b64Token = Buffer - .from(str(this.token).append('-' + Math.random()).toString()) - .toString('base64url') + return this.query().where({ token }).firstOrFail() + } - return `${config('app.frontend_url')}/auth/reset-password?token=${b64Token}` - } + /** + * get the password reset link + */ + link() { + const b64Token = Buffer.from( + str(this.token) + .append('-' + Math.random()) + .toString(), + ).toString('base64url') + + return `${config('app.frontend_url')}/auth/reset-password?token=${b64Token}` + } } diff --git a/templates/express-inertia/src/app/models/PersonalAccessToken.ts b/templates/express-inertia/src/app/models/PersonalAccessToken.ts index 89396bac..48f9e60e 100644 --- a/templates/express-inertia/src/app/models/PersonalAccessToken.ts +++ b/templates/express-inertia/src/app/models/PersonalAccessToken.ts @@ -2,25 +2,25 @@ import { PersonalAccessToken as BasePersonalAccessToken } from '@arkstack/auth' import { User } from './User' export class PersonalAccessToken extends BasePersonalAccessToken { - declare name: string - declare token: string - declare abilities: string[] - declare deviceInfo: Record - declare lastUsedAt: Date - declare expiresAt: Date - declare createdAt: Date - declare updatedAt: Date + declare name: string + declare token: string + declare abilities: string[] + declare deviceInfo: Record + declare lastUsedAt: Date + declare expiresAt: Date + declare createdAt: Date + declare updatedAt: Date - protected static columns = { - userId: 'user_id', - deviceInfo: 'device_info', - lastUsedAt: 'last_used_at', - expiresAt: 'expires_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + deviceInfo: 'device_info', + lastUsedAt: 'last_used_at', + expiresAt: 'expires_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId') - } + user() { + return this.belongsTo(User, 'userId') + } } diff --git a/templates/express-inertia/src/app/models/User.ts b/templates/express-inertia/src/app/models/User.ts index bb4e58d5..bf3b144c 100644 --- a/templates/express-inertia/src/app/models/User.ts +++ b/templates/express-inertia/src/app/models/User.ts @@ -5,32 +5,30 @@ import { UserTwoFactor } from './UserTwoFactor' import { UserFactory } from 'src/database/factories/UserFactory' export class User extends BaseUser { - declare email: string - declare password: string - declare name: string - declare createdAt: Date - declare updatedAt: Date + declare email: string + declare password: string + declare name: string + declare createdAt: Date + declare updatedAt: Date - protected static override factoryClass = UserFactory + protected static override factoryClass = UserFactory - protected static override columns = { - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static override columns = { + createdAt: 'created_at', + updatedAt: 'updated_at', + } - protected override hidden = [ - 'password' - ] + protected override hidden = ['password'] - personalAccessTokens () { - return this.hasMany(PersonalAccessToken, 'userId') - } + personalAccessTokens() { + return this.hasMany(PersonalAccessToken, 'userId') + } - twoFactor () { - return this.hasOne(UserTwoFactor, 'userId') - } + twoFactor() { + return this.hasOne(UserTwoFactor, 'userId') + } - notifications () { - return this.hasMany(UserNotification, 'userId') - } + notifications() { + return this.hasMany(UserNotification, 'userId') + } } diff --git a/templates/express-inertia/src/app/models/UserNotification.ts b/templates/express-inertia/src/app/models/UserNotification.ts index bc04dd71..a9481b41 100644 --- a/templates/express-inertia/src/app/models/UserNotification.ts +++ b/templates/express-inertia/src/app/models/UserNotification.ts @@ -2,16 +2,16 @@ import { UserNotification as BaseUserNotification } from '@arkstack/notification import { User } from './User' export class UserNotification extends BaseUserNotification { - protected static columns = { - userId: 'user_id', - actionText: 'action_text', - actionLink: 'action_link', - readAt: 'read_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + actionText: 'action_text', + actionLink: 'action_link', + readAt: 'read_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } + user() { + return this.belongsTo(User, 'userId', 'id') + } } diff --git a/templates/express-inertia/src/app/models/UserTwoFactor.ts b/templates/express-inertia/src/app/models/UserTwoFactor.ts index 6bbc87a8..8186c98a 100644 --- a/templates/express-inertia/src/app/models/UserTwoFactor.ts +++ b/templates/express-inertia/src/app/models/UserTwoFactor.ts @@ -2,19 +2,19 @@ import { UserTwoFactor as BaseUserTwoFactor } from '@arkstack/auth' import { User } from './User' export class UserTwoFactor extends BaseUserTwoFactor { - protected static columns = { - userId: 'user_id', - secretCiphertext: 'secret_ciphertext', - smsCodeHash: 'sms_code_hash', - smsCodeExpiresAt: 'sms_code_expires_at', - smsCodePurpose: 'sms_code_purpose', - enabledAt: 'enabled_at', - recoveryCodeHashes: 'recovery_code_hashes', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + secretCiphertext: 'secret_ciphertext', + smsCodeHash: 'sms_code_hash', + smsCodeExpiresAt: 'sms_code_expires_at', + smsCodePurpose: 'sms_code_purpose', + enabledAt: 'enabled_at', + recoveryCodeHashes: 'recovery_code_hashes', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } + user() { + return this.belongsTo(User, 'userId', 'id') + } } diff --git a/templates/express-inertia/src/config/app.ts b/templates/express-inertia/src/config/app.ts index 6711600a..f8cf46f0 100644 --- a/templates/express-inertia/src/config/app.ts +++ b/templates/express-inertia/src/config/app.ts @@ -1,17 +1,17 @@ import { AppConfig } from '@arkstack/common' export default (): AppConfig => { - return { - env: env('APP_ENV', 'local'), - key: env('APP_KEY', 'change-me'), - url: env('APP_URL', 'http://localhost'), - host: env('APP_HOST', '0.0.0.0'), - name: env('APP_NAME', 'Arkstack'), - frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), - debug: env('APP_DEBUG', false), - timezone: env('APP_TIMEZONE', 'UTC'), - locale: env('APP_LOCALE', 'en'), - fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), - faker_locale: env('APP_FAKER_LOCALE', 'en_US'), - } + return { + env: env('APP_ENV', 'local'), + key: env('APP_KEY', 'change-me'), + url: env('APP_URL', 'http://localhost'), + host: env('APP_HOST', '0.0.0.0'), + name: env('APP_NAME', 'Arkstack'), + frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), + debug: env('APP_DEBUG', false), + timezone: env('APP_TIMEZONE', 'UTC'), + locale: env('APP_LOCALE', 'en'), + fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), + faker_locale: env('APP_FAKER_LOCALE', 'en_US'), + } } diff --git a/templates/express-inertia/src/config/cache.ts b/templates/express-inertia/src/config/cache.ts index 4e632b05..0787ab2f 100644 --- a/templates/express-inertia/src/config/cache.ts +++ b/templates/express-inertia/src/config/cache.ts @@ -3,48 +3,48 @@ import type { CacheConfig } from '@arkstack/cache' import path from 'node:path' export default (): CacheConfig => { - return { - /** - * Default Cache Store - * - * The store used by the framework whenever a store is not explicitly - * requested. Supported drivers: memory, file, redis, database. - */ - default: env('CACHE_STORE', 'memory'), + return { + /** + * Default Cache Store + * + * The store used by the framework whenever a store is not explicitly + * requested. Supported drivers: memory, file, redis, database. + */ + default: env('CACHE_STORE', 'memory'), - /** - * Cache Key Prefix - * - * Prepended to every cache key to avoid collisions between applications - * that share the same backing store. - */ - prefix: env('CACHE_PREFIX', 'arkstack_cache_'), + /** + * Cache Key Prefix + * + * Prepended to every cache key to avoid collisions between applications + * that share the same backing store. + */ + prefix: env('CACHE_PREFIX', 'arkstack_cache_'), - /** - * Cache Stores - * - * Configure as many stores as you like. Each is referenced by name via - * `Cache.store('')`. - */ - stores: { - memory: { - driver: 'memory', - }, - file: { - driver: 'file', - path: path.join(Arkstack.rootDir(), './storage/framework/cache'), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_CACHE_DB', 1), - }, - database: { - driver: 'database', - table: env('CACHE_TABLE', 'cache'), - }, - }, - } + /** + * Cache Stores + * + * Configure as many stores as you like. Each is referenced by name via + * `Cache.store('')`. + */ + stores: { + memory: { + driver: 'memory', + }, + file: { + driver: 'file', + path: path.join(Arkstack.rootDir(), './storage/framework/cache'), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_CACHE_DB', 1), + }, + database: { + driver: 'database', + table: env('CACHE_TABLE', 'cache'), + }, + }, + } } diff --git a/templates/express-inertia/src/config/cors.ts b/templates/express-inertia/src/config/cors.ts index 7fb44d86..17ed2515 100644 --- a/templates/express-inertia/src/config/cors.ts +++ b/templates/express-inertia/src/config/cors.ts @@ -1,13 +1,15 @@ import appConfig from './app' export default () => { - const app = appConfig() + const app = appConfig() - return { - allowed_origins: [ - app.url, - app.frontend_url, - ...env('CORS_ALLOWED_ORIGINS', '').split(',').map((origin: string) => origin.trim()) - ].filter(Boolean) - } -} \ No newline at end of file + return { + allowed_origins: [ + app.url, + app.frontend_url, + ...env('CORS_ALLOWED_ORIGINS', '') + .split(',') + .map((origin: string) => origin.trim()), + ].filter(Boolean), + } +} diff --git a/templates/express-inertia/src/config/database.ts b/templates/express-inertia/src/config/database.ts index a316bbd4..46c7f535 100644 --- a/templates/express-inertia/src/config/database.ts +++ b/templates/express-inertia/src/config/database.ts @@ -1,31 +1,31 @@ import { DatabaseConfig } from '@arkstack/database' export default (): DatabaseConfig => { - return { - /** - * Default Database Connection - * - * The connection used whenever one is not explicitly requested. - * Supported drivers: postgres. - */ - default: env('DB_CONNECTION', 'pgsql'), + return { + /** + * Default Database Connection + * + * The connection used whenever one is not explicitly requested. + * Supported drivers: postgres. + */ + default: env('DB_CONNECTION', 'pgsql'), - /** - * Database Connections - * - * Credentials are provided as discrete fields. When `url` (or - * `DATABASE_URL`) is set it takes precedence over the discrete fields. - */ - connections: { - pgsql: { - driver: 'postgres', - url: env('DATABASE_URL'), - host: env('DB_HOST', '127.0.0.1'), - port: env('DB_PORT', 5432), - user: env('DB_USERNAME', 'postgres'), - database: env('DB_DATABASE', 'arkstack'), - password: env('DB_PASSWORD', 'postgres'), - }, - }, - } + /** + * Database Connections + * + * Credentials are provided as discrete fields. When `url` (or + * `DATABASE_URL`) is set it takes precedence over the discrete fields. + */ + connections: { + pgsql: { + driver: 'postgres', + url: env('DATABASE_URL'), + host: env('DB_HOST', '127.0.0.1'), + port: env('DB_PORT', 5432), + user: env('DB_USERNAME', 'postgres'), + database: env('DB_DATABASE', 'arkstack'), + password: env('DB_PASSWORD', 'postgres'), + }, + }, + } } diff --git a/templates/express-inertia/src/config/filesystem.ts b/templates/express-inertia/src/config/filesystem.ts index 849f2f34..bf209d4d 100644 --- a/templates/express-inertia/src/config/filesystem.ts +++ b/templates/express-inertia/src/config/filesystem.ts @@ -3,84 +3,87 @@ import { FilesystemConfig } from '@arkstack/filesystem' import path from 'node:path' export default (): FilesystemConfig => { - return { - /** - * Default filesystem disk to be used by the framework. - */ - default: env('FILESYSTEM_DISK', 'local'), + return { + /** + * Default filesystem disk to be used by the framework. + */ + default: env('FILESYSTEM_DISK', 'local'), - /** - * Available disks and their configurations. - * - * You can configure as many disks as you want, and even have multiple disks - * using the same driver. - * - * Supported drivers: local, ftp, s3 - */ - disks: { - local: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app'), - visibility: 'public', - }, - public: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app/public'), - visibility: 'public' - }, - ftp: { - driver: 'ftp', - host: env('FTP_HOST'), - username: env('FTP_USERNAME'), - password: env('FTP_PASSWORD'), - port: env('FTP_PORT', 21), - verbose: env('FTP_VERBOSE', false), - privateKey: env('FTP_PRIVATE_KEY'), - }, - s3: { - driver: 's3', - key: env('AWS_ACCESS_KEY_ID'), - secret: env('AWS_SECRET_ACCESS_KEY'), - region: env('AWS_DEFAULT_REGION'), - bucket: env('AWS_BUCKET'), - url: env('AWS_URL'), - endpoint: env('AWS_ENDPOINT'), - visibility: 'public', - }, - gcs: { - driver: 'gcs', - projectId: env('GOOGLE_CLOUD_PROJECT'), - keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), - bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), - visibility: 'private', - usingUniformAcl: true, - } - }, + /** + * Available disks and their configurations. + * + * You can configure as many disks as you want, and even have multiple disks + * using the same driver. + * + * Supported drivers: local, ftp, s3 + */ + disks: { + local: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app'), + visibility: 'public', + }, + public: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app/public'), + visibility: 'public', + }, + ftp: { + driver: 'ftp', + host: env('FTP_HOST'), + username: env('FTP_USERNAME'), + password: env('FTP_PASSWORD'), + port: env('FTP_PORT', 21), + verbose: env('FTP_VERBOSE', false), + privateKey: env('FTP_PRIVATE_KEY'), + }, + s3: { + driver: 's3', + key: env('AWS_ACCESS_KEY_ID'), + secret: env('AWS_SECRET_ACCESS_KEY'), + region: env('AWS_DEFAULT_REGION'), + bucket: env('AWS_BUCKET'), + url: env('AWS_URL'), + endpoint: env('AWS_ENDPOINT'), + visibility: 'public', + }, + gcs: { + driver: 'gcs', + projectId: env('GOOGLE_CLOUD_PROJECT'), + keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), + bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), + visibility: 'private', + usingUniformAcl: true, + }, + }, - /** - * Optional symbolic links to create when the `storage:link` command is executed. - * The key of the object represents the link location, while the value represents the target location. - */ - links: { - [path.join(Arkstack.rootDir(), './public/storage')]: path.join(Arkstack.rootDir(), './storage/app/public'), - }, + /** + * Optional symbolic links to create when the `storage:link` command is executed. + * The key of the object represents the link location, while the value represents the target location. + */ + links: { + [path.join(Arkstack.rootDir(), './public/storage')]: path.join( + Arkstack.rootDir(), + './storage/app/public', + ), + }, - /** - * Optional file name generator function. - * If provided, this function will be used to generate unique file names - * for uploaded files when using the `saveFile` method of the Storage class. - * - * @param originalName The original name of the file - * @returns A unique file name - * - * fileNameGenerator: (originalName: string) => { - * const timestamp = Date.now() - * const randomString = Math.random().toString(36).substring(2, 8) - * const extension = path.extname(originalName) - * const baseName = path.basename(originalName, extension) - * - * return `${baseName}-${timestamp}-${randomString}${extension}` - * }, - */ - } + /** + * Optional file name generator function. + * If provided, this function will be used to generate unique file names + * for uploaded files when using the `saveFile` method of the Storage class. + * + * @param originalName The original name of the file + * @returns A unique file name + * + * fileNameGenerator: (originalName: string) => { + * const timestamp = Date.now() + * const randomString = Math.random().toString(36).substring(2, 8) + * const extension = path.extname(originalName) + * const baseName = path.basename(originalName, extension) + * + * return `${baseName}-${timestamp}-${randomString}${extension}` + * }, + */ + } } diff --git a/templates/express-inertia/src/config/middleware.ts b/templates/express-inertia/src/config/middleware.ts index b2a90e32..8031acce 100644 --- a/templates/express-inertia/src/config/middleware.ts +++ b/templates/express-inertia/src/config/middleware.ts @@ -29,10 +29,8 @@ export default (): MiddlewareConfig => { (req, _res, next) => { useExpressUploadContext(req) next() - } - ], - after: [ - requestLogger() + }, ], + after: [requestLogger()], } -} +} diff --git a/templates/express-inertia/src/config/notifications.ts b/templates/express-inertia/src/config/notifications.ts index 14d8a10b..7a745246 100644 --- a/templates/express-inertia/src/config/notifications.ts +++ b/templates/express-inertia/src/config/notifications.ts @@ -3,157 +3,157 @@ import { NotificationConfig } from '@arkstack/notifications' import { join } from 'node:path' export default (): NotificationConfig => { - return { - /** - * Default Notification Driver - * - * The default notifications driver that is utilized for outgoing messages. - */ - default_driver: env('NOTIFICATION_DRIVER', 'mail'), + return { + /** + * Default Notification Driver + * + * The default notifications driver that is utilized for outgoing messages. + */ + default_driver: env('NOTIFICATION_DRIVER', 'mail'), + /** + * Notification Drivers + * + * Notifications will be sent using one of the configured drivers. + */ + drivers: { + mail: { + transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', + from: { + name: env('MAIL_FROM_NAME', 'Arcstack'), + address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), + }, + test_address: env('MAIL_TEST_ADDRESS'), + }, + sms: { + transport: 'africastalking', + from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), + }, + db: { + table: 'user_notifications', + }, + realtime: { /** - * Notification Drivers - * - * Notifications will be sent using one of the configured drivers. + * pusher | firebase | firebase-database | polling | memory */ - drivers: { - mail: { - transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', - from: { - name: env('MAIL_FROM_NAME', 'Arcstack'), - address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), - }, - test_address: env('MAIL_TEST_ADDRESS'), - }, - sms: { - transport: 'africastalking', - from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), - }, - db: { - table: 'user_notifications', - }, - realtime: { - /** - * pusher | firebase | firebase-database | polling | memory - */ - transport: env('REALTIME_TRANSPORT', 'pusher'), - channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), - event: env('REALTIME_EVENT', 'notification'), - store: env('REALTIME_STORE', false), - }, + transport: env('REALTIME_TRANSPORT', 'pusher'), + channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), + event: env('REALTIME_EVENT', 'notification'), + store: env('REALTIME_STORE', false), + }, + }, + transports: { + /** + * SMTP Notifications Transport + * + * Outgoing notifications will be sent as mail using SMTP. + */ + smtp: { + host: env('MAIL_HOST', 'localhost'), + port: env('MAIL_PORT', 1025), + secure: env('MAIL_SECURE', false), + auth: { + user: env('MAIL_USERNAME', 'user@example.com'), + pass: env('MAIL_PASSWORD', 'password'), }, - transports: { - /** - * SMTP Notifications Transport - * - * Outgoing notifications will be sent as mail using SMTP. - */ - smtp: { - host: env('MAIL_HOST', 'localhost'), - port: env('MAIL_PORT', 1025), - secure: env('MAIL_SECURE', false), - auth: { - user: env('MAIL_USERNAME', 'user@example.com'), - pass: env('MAIL_PASSWORD', 'password'), - }, - }, + }, - /** - * File Notifications Transport - * - * Outgoing notifications will be sent as mail but stored locally in a file - * for testing purposes. - */ - file: { - /** - * Mails Directory - * - * Outgoing mails will be intercepted and stored in this directory. - */ - directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), - }, + /** + * File Notifications Transport + * + * Outgoing notifications will be sent as mail but stored locally in a file + * for testing purposes. + */ + file: { + /** + * Mails Directory + * + * Outgoing mails will be intercepted and stored in this directory. + */ + directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), + }, - /** - * Africa's Talking Notifications Transport - * - * Outgoing notifications will be sent as SMS via Africa's Talking. - */ - africastalking: { - username: env('AFRICASTALKING_USERNAME', 'sandbox'), - apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), - senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), - }, + /** + * Africa's Talking Notifications Transport + * + * Outgoing notifications will be sent as SMS via Africa's Talking. + */ + africastalking: { + username: env('AFRICASTALKING_USERNAME', 'sandbox'), + apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), + senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), + }, - /** - * Twilio Notifications Transport - * - * Outgoing notifications will be sent as SMS via Twilio. - */ - twilio: { - accountSid: env('TWILIO_ACCOUNT_SID'), - authToken: env('TWILIO_AUTH_TOKEN'), - from: env('TWILIO_FROM', env('SMS_FROM')), - }, + /** + * Twilio Notifications Transport + * + * Outgoing notifications will be sent as SMS via Twilio. + */ + twilio: { + accountSid: env('TWILIO_ACCOUNT_SID'), + authToken: env('TWILIO_AUTH_TOKEN'), + from: env('TWILIO_FROM', env('SMS_FROM')), + }, - /** - * Pusher Realtime Transport - * - * Realtime notifications will be broadcast over Pusher Channels. - */ - pusher: { - app_id: env('PUSHER_APP_ID', ''), - key: env('PUSHER_KEY', ''), - secret: env('PUSHER_SECRET', ''), - cluster: env('PUSHER_CLUSTER', 'mt1'), - use_tls: env('PUSHER_USE_TLS', true), - }, + /** + * Pusher Realtime Transport + * + * Realtime notifications will be broadcast over Pusher Channels. + */ + pusher: { + app_id: env('PUSHER_APP_ID', ''), + key: env('PUSHER_KEY', ''), + secret: env('PUSHER_SECRET', ''), + cluster: env('PUSHER_CLUSTER', 'mt1'), + use_tls: env('PUSHER_USE_TLS', true), + }, - /** - * Firebase Realtime Transport - * - * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. - */ - firebase: { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - web: { - api_key: env('FIREBASE_API_KEY', ''), - app_id: env('FIREBASE_APP_ID', ''), - messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), - vapid_key: env('FIREBASE_VAPID_KEY', ''), - }, - }, + /** + * Firebase Realtime Transport + * + * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. + */ + firebase: { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + web: { + api_key: env('FIREBASE_API_KEY', ''), + app_id: env('FIREBASE_APP_ID', ''), + messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), + vapid_key: env('FIREBASE_VAPID_KEY', ''), + }, + }, - /** - * Firebase Realtime Database Transport - * - * Realtime notifications will be broadcast over Realtime Database channels. - */ - 'firebase-database': { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - database_url: env('FIREBASE_DATABASE_URL', ''), - web: { - api_key: env('FIREBASE_API_KEY', ''), - auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), - app_id: env('FIREBASE_APP_ID', ''), - }, - }, + /** + * Firebase Realtime Database Transport + * + * Realtime notifications will be broadcast over Realtime Database channels. + */ + 'firebase-database': { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + database_url: env('FIREBASE_DATABASE_URL', ''), + web: { + api_key: env('FIREBASE_API_KEY', ''), + auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), + app_id: env('FIREBASE_APP_ID', ''), + }, + }, - /** - * Polling Realtime Transport - * - * Nothing is pushed; clients poll on this interval (ms) instead. - */ - polling: { - interval: env('REALTIME_POLL_INTERVAL', 5000), - }, - } - } + /** + * Polling Realtime Transport + * + * Nothing is pushed; clients poll on this interval (ms) instead. + */ + polling: { + interval: env('REALTIME_POLL_INTERVAL', 5000), + }, + }, + } } diff --git a/templates/express-inertia/src/config/queue.ts b/templates/express-inertia/src/config/queue.ts index f1bd0566..68cf03fe 100644 --- a/templates/express-inertia/src/config/queue.ts +++ b/templates/express-inertia/src/config/queue.ts @@ -1,43 +1,43 @@ import type { QueueConfig } from '@arkstack/queue' export default (): QueueConfig => { - return { - /** - * Default Queue Connection - * - * The connection used when a job is dispatched without specifying one. - * The `sync` connection runs jobs immediately (no worker required) and is - * a sensible default for local development and tests. - * - * Supported drivers: sync, database, redis. - */ - default: env('QUEUE_CONNECTION', 'sync'), + return { + /** + * Default Queue Connection + * + * The connection used when a job is dispatched without specifying one. + * The `sync` connection runs jobs immediately (no worker required) and is + * a sensible default for local development and tests. + * + * Supported drivers: sync, database, redis. + */ + default: env('QUEUE_CONNECTION', 'sync'), - /** - * Queue Connections - * - * Configure a connection per backend. Run a worker with `ark queue:work` - * to process jobs on the database and redis connections. - */ - connections: { - sync: { - driver: 'sync', - }, - database: { - driver: 'database', - table: env('QUEUE_TABLE', 'jobs'), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_QUEUE_DB', 0), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - }, - } + /** + * Queue Connections + * + * Configure a connection per backend. Run a worker with `ark queue:work` + * to process jobs on the database and redis connections. + */ + connections: { + sync: { + driver: 'sync', + }, + database: { + driver: 'database', + table: env('QUEUE_TABLE', 'jobs'), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_QUEUE_DB', 0), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + }, + } } diff --git a/templates/express-inertia/src/config/resources.ts b/templates/express-inertia/src/config/resources.ts index 0561b2b9..3ab17a98 100644 --- a/templates/express-inertia/src/config/resources.ts +++ b/templates/express-inertia/src/config/resources.ts @@ -9,44 +9,44 @@ import type { ResoraConfig } from 'resora' * resora accepts at runtime but doesn't surface in its narrower config type. */ export default (): ResoraConfig => { - return { - preferredCase: 'camel', - responseStructure: { - wrap: true, - rootKey: 'data', - }, - paginatedExtras: ['meta', 'links'], - baseUrl: env('APP_URL', 'http://localhost'), - pageName: 'page', - paginatedLinks: { - first: 'first', - last: 'last', - prev: 'prev', - next: 'next', - }, - paginatedMeta: { - to: 'to', - from: 'from', - links: 'links', - path: 'path', - total: 'total', - per_page: 'per_page', - last_page: 'last_page', - current_page: 'current_page', - }, - cursorMeta: { - previous: 'previous', - next: 'next', - }, - resourcesDir: 'src/app/http/resources', - localStubsDir: 'node_modules/@arkstack/driver-express/stubs', - stubs: { - resource: 'resource.stub', - collection: 'resource.collection.stub', - controller: 'controller.stub', - api: 'controller.api.stub', - model: 'controller.model.stub', - apiResource: 'controller.api.resource.stub', - }, - } as ResoraConfig + return { + preferredCase: 'camel', + responseStructure: { + wrap: true, + rootKey: 'data', + }, + paginatedExtras: ['meta', 'links'], + baseUrl: env('APP_URL', 'http://localhost'), + pageName: 'page', + paginatedLinks: { + first: 'first', + last: 'last', + prev: 'prev', + next: 'next', + }, + paginatedMeta: { + to: 'to', + from: 'from', + links: 'links', + path: 'path', + total: 'total', + per_page: 'per_page', + last_page: 'last_page', + current_page: 'current_page', + }, + cursorMeta: { + previous: 'previous', + next: 'next', + }, + resourcesDir: 'src/app/http/resources', + localStubsDir: 'node_modules/@arkstack/driver-express/stubs', + stubs: { + resource: 'resource.stub', + collection: 'resource.collection.stub', + controller: 'controller.stub', + api: 'controller.api.stub', + model: 'controller.model.stub', + apiResource: 'controller.api.resource.stub', + }, + } as ResoraConfig } diff --git a/templates/express-inertia/src/config/session.ts b/templates/express-inertia/src/config/session.ts index 0a829e18..1f5fa62d 100644 --- a/templates/express-inertia/src/config/session.ts +++ b/templates/express-inertia/src/config/session.ts @@ -4,83 +4,80 @@ import { Str } from '@h3ravel/support' import { join } from 'node:path' export default (): SessionConfig => { - return { - secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), + return { + secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), - /** - * Session Driver - * - * The default session driver that is utilized for incoming requests. - * - * Supported: "file", "cookie", "database", - * (WIP: "apc", "memcached", "redis", "dynamodb", "array") - */ - driver: env('SESSION_DRIVER', 'file'), + /** + * Session Driver + * + * The default session driver that is utilized for incoming requests. + * + * Supported: "file", "cookie", "database", + * (WIP: "apc", "memcached", "redis", "dynamodb", "array") + */ + driver: env('SESSION_DRIVER', 'file'), - /** - * Cookie Name - * - * The name of the session cookie that is created by Arkstack. - */ - cookie: env( - 'SESSION_COOKIE', - Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session' - ), + /** + * Cookie Name + * + * The name of the session cookie that is created by Arkstack. + */ + cookie: env('SESSION_COOKIE', Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session'), - /** - * Session Time To Live - * - * The number of minutes that the session is allowed to remain idle before - * it expires. - */ - ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), + /** + * Session Time To Live + * + * The number of minutes that the session is allowed to remain idle before + * it expires. + */ + ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), - /** - * HTTP Only Access - * - * Prevent JavaScript from accessing the value of the cookie making it - * only accessible through the HTTP protocol. - */ - http_only: true, + /** + * HTTP Only Access + * + * Prevent JavaScript from accessing the value of the cookie making it + * only accessible through the HTTP protocol. + */ + http_only: true, - /** - * HTTPS Only Access - * - * This option ensures that cookies will only be sent back to the server - * if the browser has a HTTPS connection if set to "true". - */ - secure: env('NODE_ENV', 'development') === 'production', + /** + * HTTPS Only Access + * + * This option ensures that cookies will only be sent back to the server + * if the browser has a HTTPS connection if set to "true". + */ + secure: env('NODE_ENV', 'development') === 'production', - /** - * Same-Site Cookies - * - * Determines how cookies behave when cross-site requests take - * place, and can be used to mitigate CSRF attacks. - * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - * - * Supported: "Lax", "Strict", "None", undefined - */ - same_site: 'Lax', + /** + * Same-Site Cookies + * + * Determines how cookies behave when cross-site requests take + * place, and can be used to mitigate CSRF attacks. + * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + * + * Supported: "Lax", "Strict", "None", undefined + */ + same_site: 'Lax', - /** - * Session Cookie Path - * - * Determines the path for which the cookie will be available. - */ - path: '/', - /** - * Datbase Table - * - * For the "database" session driver, this will determine the table to - * be used to store sessions. - */ - table: env('SESSION_TABLE', 'sessions'), + /** + * Session Cookie Path + * + * Determines the path for which the cookie will be available. + */ + path: '/', + /** + * Datbase Table + * + * For the "database" session driver, this will determine the table to + * be used to store sessions. + */ + table: env('SESSION_TABLE', 'sessions'), - /** - * Session Directory - * - * For the "file" session driver, the session files are placed in this directory. - */ - directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), - } + /** + * Session Directory + * + * For the "file" session driver, the session files are placed in this directory. + */ + directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), + } } diff --git a/templates/express-inertia/src/core/app.ts b/templates/express-inertia/src/core/app.ts index fbd9315a..8129b712 100644 --- a/templates/express-inertia/src/core/app.ts +++ b/templates/express-inertia/src/core/app.ts @@ -6,11 +6,11 @@ import { type Express, type Handler } from 'express' export default class Application extends Arkstack { /** - * Creates an instance of the Application class, initializing - * the Express driver with the provided options and creating an Express + * Creates an instance of the Application class, initializing + * the Express driver with the provided options and creating an Express * application instance. - * - * @param app + * + * @param app */ constructor(app?: Express) { super() @@ -28,10 +28,10 @@ export default class Application extends Arkstack { /** * Gets the ArkstackRouterContract implementation for the Express framework. - * - * @returns + * + * @returns */ - getRouter (): ArkstackRouterContract { + getRouter(): ArkstackRouterContract { return { bind: (_app: Express) => Router.bind(), list: (options: ArkstackRouteListOptions = {}) => Router.list(options), @@ -39,20 +39,20 @@ export default class Application extends Arkstack { } /** - * Boots the application by mounting public assets, binding the + * Boots the application by mounting public assets, binding the * router, applying middleware, and starting the server. - * + * * @param port The numeric port to run the server on * @param defer Set to true to skip server startup */ - public async boot (port: number, defer = false) { + public async boot(port: number, defer = false) { // Load public assets await this.driver.mountPublicAssets(this.app, path.join(Arkstack.rootDir(), 'public')) // Apply all middleware await this.driver.applyMiddleware(this.app, config('middleware') as any) - // Bind the router + // Bind the router await this.driver.bindRouter(this.app) // Error Handler diff --git a/templates/express-inertia/src/database/factories/UserFactory.ts b/templates/express-inertia/src/database/factories/UserFactory.ts index ce2528af..7076c8b8 100644 --- a/templates/express-inertia/src/database/factories/UserFactory.ts +++ b/templates/express-inertia/src/database/factories/UserFactory.ts @@ -4,13 +4,13 @@ import { User } from '@app/models/User' import { faker } from '@faker-js/faker' export class UserFactory extends ModelFactory { - protected model = User + protected model = User - protected definition (_sequence: number) { - return { - 'name': faker.person.fullName(), - 'email': faker.internet.email(), - 'password': Hash.make('password'), - } + protected definition(_sequence: number) { + return { + name: faker.person.fullName(), + email: faker.internet.email(), + password: Hash.make('password'), } + } } diff --git a/templates/express-inertia/src/database/migrations/20260428164120_create_users_table.ts b/templates/express-inertia/src/database/migrations/20260428164120_create_users_table.ts index 8c6f52ac..cb489074 100644 --- a/templates/express-inertia/src/database/migrations/20260428164120_create_users_table.ts +++ b/templates/express-inertia/src/database/migrations/20260428164120_create_users_table.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUsersTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('users', (table) => { - table.id('id', 'uuid').primary() - table.string('name') - table.string('email').unique().index() - table.string('password') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("users", (table) => { + table.id("id", "uuid").primary(); + table.string("name"); + table.string("email").unique().index(); + table.string("password"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('users') - } -} \ No newline at end of file + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("users"); + } +} diff --git a/templates/express-inertia/src/database/migrations/20260428164833_createpasswordresetstable.ts b/templates/express-inertia/src/database/migrations/20260428164833_createpasswordresetstable.ts index 9315d10b..f6dc4083 100644 --- a/templates/express-inertia/src/database/migrations/20260428164833_createpasswordresetstable.ts +++ b/templates/express-inertia/src/database/migrations/20260428164833_createpasswordresetstable.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePasswordResetsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('password_resets', (table) => { - table.id('id', 'uuid').primary() - table.string('email').nullable().index() - table.string('phone').nullable().index() - table.string('token') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("password_resets", (table) => { + table.id("id", "uuid").primary(); + table.string("email").nullable().index(); + table.string("phone").nullable().index(); + table.string("token"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('password_resets') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("password_resets"); + } } diff --git a/templates/express-inertia/src/database/migrations/20260428165239_create_person_access_tokens_table.ts b/templates/express-inertia/src/database/migrations/20260428165239_create_person_access_tokens_table.ts index 252d0f47..45e1249b 100644 --- a/templates/express-inertia/src/database/migrations/20260428165239_create_person_access_tokens_table.ts +++ b/templates/express-inertia/src/database/migrations/20260428165239_create_person_access_tokens_table.ts @@ -1,23 +1,29 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePersonAccessTokensTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('personal_access_tokens', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('personalAccessTokens') - table.string('name') - table.text('token').unique() - table.json('abilities').nullable() - table.json('deviceInfo').nullable().map('device_info') - table.date('lastUsedAt').nullable().map('last_used_at') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("personal_access_tokens", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("personalAccessTokens"); + table.string("name"); + table.text("token").unique(); + table.json("abilities").nullable(); + table.json("deviceInfo").nullable().map("device_info"); + table.date("lastUsedAt").nullable().map("last_used_at"); + table.date("expiresAt").nullable().map("expires_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('personal_access_tokens') - } -} + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("personal_access_tokens"); + } +} diff --git a/templates/express-inertia/src/database/migrations/20260505110000_create_user_two_factors_table.ts b/templates/express-inertia/src/database/migrations/20260505110000_create_user_two_factors_table.ts index 7542293b..1647dda3 100644 --- a/templates/express-inertia/src/database/migrations/20260505110000_create_user_two_factors_table.ts +++ b/templates/express-inertia/src/database/migrations/20260505110000_create_user_two_factors_table.ts @@ -1,24 +1,33 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserTwoFactorsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_two_factors', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('twoFactor') - table.enum('method', ['authenticator', 'sms']).nullable().enumName('TwoFactorMethod') - table.string('secretCiphertext').nullable().map('secret_ciphertext') - table.string('smsCodeHash').nullable().map('sms_code_hash') - table.date('smsCodeExpiresAt').nullable().map('sms_code_expires_at') - table.string('smsCodePurpose').nullable().map('sms_code_purpose') - table.date('enabledAt').nullable().map('enabled_at') - table.json('recoveryCodeHashes').nullable().map('recovery_code_hashes') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_two_factors", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("twoFactor"); + table + .enum("method", ["authenticator", "sms"]) + .nullable() + .enumName("TwoFactorMethod"); + table.string("secretCiphertext").nullable().map("secret_ciphertext"); + table.string("smsCodeHash").nullable().map("sms_code_hash"); + table.date("smsCodeExpiresAt").nullable().map("sms_code_expires_at"); + table.string("smsCodePurpose").nullable().map("sms_code_purpose"); + table.date("enabledAt").nullable().map("enabled_at"); + table.json("recoveryCodeHashes").nullable().map("recovery_code_hashes"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_two_factors') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_two_factors"); + } } diff --git a/templates/express-inertia/src/database/migrations/20260505111000_create_user_notifications_table.ts b/templates/express-inertia/src/database/migrations/20260505111000_create_user_notifications_table.ts index 7690899b..8234a972 100644 --- a/templates/express-inertia/src/database/migrations/20260505111000_create_user_notifications_table.ts +++ b/templates/express-inertia/src/database/migrations/20260505111000_create_user_notifications_table.ts @@ -1,24 +1,30 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserNotificationsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_notifications', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('notifications') - table.string('type').nullable() - table.string('title') - table.text('description') - table.string('actionText').nullable().map('action_text') - table.string('actionLink').nullable().map('action_link') - table.json('meta').nullable() - table.date('readAt').nullable().map('read_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_notifications", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("notifications"); + table.string("type").nullable(); + table.string("title"); + table.text("description"); + table.string("actionText").nullable().map("action_text"); + table.string("actionLink").nullable().map("action_link"); + table.json("meta").nullable(); + table.date("readAt").nullable().map("read_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_notifications') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_notifications"); + } } diff --git a/templates/express-inertia/src/database/migrations/20260524000000_create_sessions_table.ts b/templates/express-inertia/src/database/migrations/20260524000000_create_sessions_table.ts index d61c2d64..e3f71385 100644 --- a/templates/express-inertia/src/database/migrations/20260524000000_create_sessions_table.ts +++ b/templates/express-inertia/src/database/migrations/20260524000000_create_sessions_table.ts @@ -1,16 +1,16 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateSessionsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('sessions', (table) => { - table.string('id').primary() - table.text('payload') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("sessions", (table) => { + table.string("id").primary(); + table.text("payload"); + table.date("expiresAt").nullable().map("expires_at"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('sessions') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("sessions"); + } } diff --git a/templates/express-inertia/src/database/seeders/DatabaseSeeder.ts b/templates/express-inertia/src/database/seeders/DatabaseSeeder.ts index 2e3d0cb6..23079b78 100644 --- a/templates/express-inertia/src/database/seeders/DatabaseSeeder.ts +++ b/templates/express-inertia/src/database/seeders/DatabaseSeeder.ts @@ -2,9 +2,7 @@ import { Seeder } from '@arkstack/database' import { UserSeeder } from './UserSeeder' export class DatabaseSeeder extends Seeder { - public async run (): Promise { - this.call([ - UserSeeder - ]) - } + public async run(): Promise { + this.call([UserSeeder]) + } } diff --git a/templates/express-inertia/src/database/seeders/UserSeeder.ts b/templates/express-inertia/src/database/seeders/UserSeeder.ts index 66b32f16..fcfb7800 100644 --- a/templates/express-inertia/src/database/seeders/UserSeeder.ts +++ b/templates/express-inertia/src/database/seeders/UserSeeder.ts @@ -2,7 +2,7 @@ import { Seeder } from '@arkstack/database' import { User } from 'src/app/models/User' export class UserSeeder extends Seeder { - public async run (): Promise { - User.factory(5).create() - } + public async run(): Promise { + User.factory(5).create() + } } diff --git a/templates/express-inertia/src/routes/console.ts b/templates/express-inertia/src/routes/console.ts index bbb354e6..c872552f 100644 --- a/templates/express-inertia/src/routes/console.ts +++ b/templates/express-inertia/src/routes/console.ts @@ -12,5 +12,7 @@ import { Schedule } from '@arkstack/scheduler' */ // Schedule.command('cache:prune').hourly() -Schedule.call(() => console.log('tick')).everyFiveMinutes().withoutOverlapping() +Schedule.call(() => console.log('tick')) + .everyFiveMinutes() + .withoutOverlapping() // Schedule.exec('backup.sh').dailyAt('01:30').onOneServer() diff --git a/templates/express-inertia/src/routes/web.ts b/templates/express-inertia/src/routes/web.ts index f9e5ef5a..37927c19 100644 --- a/templates/express-inertia/src/routes/web.ts +++ b/templates/express-inertia/src/routes/web.ts @@ -6,4 +6,4 @@ Router.get('/', async () => { title: 'Welcome to Arkstack', message: 'Server running — ready for requests', }) -}) +}) diff --git a/templates/express-inertia/tests/basic.test.ts b/templates/express-inertia/tests/basic.test.ts index 5e60252d..50d1747a 100644 --- a/templates/express-inertia/tests/basic.test.ts +++ b/templates/express-inertia/tests/basic.test.ts @@ -15,15 +15,15 @@ const rwd = path.join(process.cwd(), '/templates/express') if (existsSync(rwd)) Arkstack.setRootDir(rwd) describe('starter template', () => { - it('runs a basic assertion', () => { - expect(1 + 1).toBe(2) - }) + it('runs a basic assertion', () => { + expect(1 + 1).toBe(2) + }) - it('can test a route', async () => { - await request(app).get('/api').contains('{"status":"OK"}') - }) + it('can test a route', async () => { + await request(app).get('/api').contains('{"status":"OK"}') + }) - it('can test an api route', async () => { - await request(app).get('/api/users').contains('"status":"success","message":"OK"') - }) + it('can test an api route', async () => { + await request(app).get('/api/users').contains('"status":"success","message":"OK"') + }) }) diff --git a/templates/express-inertia/tests/decorators.test.ts b/templates/express-inertia/tests/decorators.test.ts index ef3c54c3..2cb652a0 100644 --- a/templates/express-inertia/tests/decorators.test.ts +++ b/templates/express-inertia/tests/decorators.test.ts @@ -10,39 +10,39 @@ import { Router } from 'clear-router/express' import { request } from 'parasito' describe('@Bind() — Controllers', () => { - let app: express.Application - let router: ExRouter - - const setupApp = async (): Promise => { - Router.apply(router) - app.use(router) - } - - beforeEach(() => { - Router.reset() - Container.clear() - - app = express() - router = ExRouter() - app.use(express.json()) - }) - - class UserController extends BaseController { - @Bind() - async test (req: Request) { - return req.url ?? '' - } + let app: express.Application + let router: ExRouter + + const setupApp = async (): Promise => { + Router.apply(router) + app.use(router) + } + + beforeEach(() => { + Router.reset() + Container.clear() + + app = express() + router = ExRouter() + app.use(express.json()) + }) + + class UserController extends BaseController { + @Bind() + async test(req: Request) { + return req.url ?? '' } - - it('can setup basic route', async () => { - Router.get('/test', ({ res }) => res.send('OK')) - await setupApp() - await request(app).get('/test').expect(200).expect('OK') - }) - - it('can use container injection in controllers', async () => { - Router.get('/test/:id', [UserController, 'test']) - await setupApp() - await request(app).get('/test/1541').expect(200).expect('/test/1541') - }) -}) \ No newline at end of file + } + + it('can setup basic route', async () => { + Router.get('/test', ({ res }) => res.send('OK')) + await setupApp() + await request(app).get('/test').expect(200).expect('OK') + }) + + it('can use container injection in controllers', async () => { + Router.get('/test/:id', [UserController, 'test']) + await setupApp() + await request(app).get('/test/1541').expect(200).expect('/test/1541') + }) +}) diff --git a/templates/express-inertia/tsconfig.json b/templates/express-inertia/tsconfig.json index 4e58f62f..398f2802 100644 --- a/templates/express-inertia/tsconfig.json +++ b/templates/express-inertia/tsconfig.json @@ -3,10 +3,5 @@ "compilerOptions": { "jsx": "react-jsx" }, - "include": [ - ".arkstack/*.d.ts", - "src", - "tests", - "resources" - ] + "include": [".arkstack/*.d.ts", "src", "tests", "resources"] } diff --git a/templates/express-inertia/tsdown.config.ts b/templates/express-inertia/tsdown.config.ts index 85a14f96..dd003efa 100644 --- a/templates/express-inertia/tsdown.config.ts +++ b/templates/express-inertia/tsdown.config.ts @@ -23,7 +23,10 @@ export default defineConfig([ deps: { skipNodeModulesBundle: true, }, - watch: env === 'dev' && process.env.CLI_BUILD !== 'true' ? ['.env', '.env.*', 'src', 'tsconfig.json'] : false, + watch: + env === 'dev' && process.env.CLI_BUILD !== 'true' + ? ['.env', '.env.*', 'src', 'tsconfig.json'] + : false, plugins: [ devServer({ env: Object.assign({}, process.env, { @@ -31,7 +34,7 @@ export default defineConfig([ }), execArgv: ['-r', 'source-map-support/register'], input: path.join(Arkstack.rootDir(), 'src/server.ts'), - }) + }), ], outExtensions: () => { return { diff --git a/templates/express-inertia/vite.config.ts b/templates/express-inertia/vite.config.ts index c908f549..c99c978c 100644 --- a/templates/express-inertia/vite.config.ts +++ b/templates/express-inertia/vite.config.ts @@ -2,11 +2,13 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ - plugins: [react()], - publicDir: false, - build: { - manifest: true, - outDir: 'public/build', - rolldownOptions: { input: ['resources/css/app.css', 'resources/js/app.tsx'] }, + plugins: [react()], + publicDir: false, + build: { + manifest: true, + outDir: 'public/build', + rolldownOptions: { + input: ['resources/css/app.css', 'resources/js/app.tsx'], }, -}) \ No newline at end of file + }, +}) diff --git a/templates/express-inertia/vitest.config.ts b/templates/express-inertia/vitest.config.ts index b10989bd..59d953a2 100644 --- a/templates/express-inertia/vitest.config.ts +++ b/templates/express-inertia/vitest.config.ts @@ -1,21 +1,21 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ - oxc: { - decorator: { - legacy: true, - emitDecoratorMetadata: true, - } + oxc: { + decorator: { + legacy: true, + emitDecoratorMetadata: true, }, - resolve: { - tsconfigPaths: true - }, - test: { - environment: 'node', - include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - env: { - NODE_ENV: 'test', - VERBOSITY: '0' - }, + }, + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + env: { + NODE_ENV: 'test', + VERBOSITY: '0', }, + }, }) diff --git a/templates/express/eslint.config.js b/templates/express/eslint.config.js index cfa6a459..b7f24597 100644 --- a/templates/express/eslint.config.js +++ b/templates/express/eslint.config.js @@ -1,72 +1,74 @@ import { defineConfig } from 'eslint/config' +import eslintConfigPrettier from 'eslint-config-prettier' import { globalIgnores } from 'eslint/config' import js from '@eslint/js' import markdown from '@eslint/markdown' import tseslint from 'typescript-eslint' export default defineConfig( - { - languageOptions: { - globals: { - console: 'readonly', - process: 'readonly', - }, - parserOptions: { - tsconfigRootDir: process.cwd() - } - }, + { + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + }, + parserOptions: { + tsconfigRootDir: process.cwd(), + }, }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ['docs/**/*.md', 'README.md'], - plugins: { - markdown, - }, - extends: ['markdown/recommended'], - rules: { - 'no-irregular-whitespace': 'off', - 'markdown/no-missing-label-refs': 'off', - } + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['docs/**/*.md', 'README.md'], + plugins: { + markdown, + }, + extends: ['markdown/recommended'], + rules: { + 'no-irregular-whitespace': 'off', + 'markdown/no-missing-label-refs': 'off', }, - [ - globalIgnores([ - 'docs/.vitepress/**', - 'bin/**', - 'dist/**', - 'build/**', - '.arkormx/**', - '.arkstack/**', - 'node_modules/**', - ]) - ], - { - rules: { - 'brace-style': [ - 'error', - '1tbs', - { 'allowSingleLine': false }, - ], - 'no-console': 'off', - 'no-thenable': 'off', - // 'no-ternary': 'error', - 'newline-before-return': 'error', - 'semi': ['error', 'never'], - 'quotes': ['error', 'single'], - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', { - 'argsIgnorePattern': '^_|_', - 'vars': 'all', - 'args': 'after-used', - 'ignoreRestSiblings': false, - 'varsIgnorePattern': '^I[A-Z]|^_', - } - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/triple-slash-reference': ['error', { - 'path': 'always' - }] - } + }, + [ + globalIgnores([ + 'docs/.vitepress/**', + 'bin/**', + 'dist/**', + 'build/**', + '.arkormx/**', + '.arkstack/**', + 'node_modules/**', + ]), + ], + { + rules: { + 'brace-style': ['error', '1tbs', { allowSingleLine: false }], + 'no-console': 'off', + 'no-thenable': 'off', + // 'no-ternary': 'error', + 'newline-before-return': 'error', + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_|_', + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + varsIgnorePattern: '^I[A-Z]|^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + }, + ], }, -) \ No newline at end of file + }, + eslintConfigPrettier, +) diff --git a/templates/express/public/index.json b/templates/express/public/index.json index 3c1e30e8..9d163840 100644 --- a/templates/express/public/index.json +++ b/templates/express/public/index.json @@ -18,4 +18,4 @@ "p": "See the world through our own eyes." } } -} \ No newline at end of file +} diff --git a/templates/express/src/app/http/controllers/BaseController.ts b/templates/express/src/app/http/controllers/BaseController.ts index 43e864dd..7588ba10 100644 --- a/templates/express/src/app/http/controllers/BaseController.ts +++ b/templates/express/src/app/http/controllers/BaseController.ts @@ -4,18 +4,18 @@ import { Controller } from 'clear-router' import { HttpContext } from 'clear-router/types/express' export class BaseController extends Controller { - async validate, R extends RulesForData> ( - rules: R, - camelizeKeys = false - ) { + async validate, R extends RulesForData>(rules: R, camelizeKeys = false) { const data = await Validator.make(this.body, rules).validate() if (camelizeKeys) { - return Object.keys(data).reduce((result, key) => { - result[str(key).camel().toString()] = data[key as never] + return Object.keys(data).reduce( + (result, key) => { + result[str(key).camel().toString()] = data[key as never] - return result - }, {} as Record) as { [K in keyof R]: any } + return result + }, + {} as Record, + ) as { [K in keyof R]: any } } return data as { [K in keyof R]: any } diff --git a/templates/express/src/app/http/controllers/UserController.ts b/templates/express/src/app/http/controllers/UserController.ts index 352af7ac..88aec5bf 100644 --- a/templates/express/src/app/http/controllers/UserController.ts +++ b/templates/express/src/app/http/controllers/UserController.ts @@ -81,7 +81,7 @@ export default class UserController extends BaseController { } /** - * Delete a specific resource + * Delete a specific resource */ async destroy() { return new Resource({}) diff --git a/templates/express/src/app/http/resources/UserCollection.ts b/templates/express/src/app/http/resources/UserCollection.ts index a960e666..eedc2686 100644 --- a/templates/express/src/app/http/resources/UserCollection.ts +++ b/templates/express/src/app/http/resources/UserCollection.ts @@ -10,7 +10,7 @@ export class UserCollection extends ResourceCollection { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/express/src/app/http/resources/UserResource.ts b/templates/express/src/app/http/resources/UserResource.ts index f4de6048..e5247c06 100644 --- a/templates/express/src/app/http/resources/UserResource.ts +++ b/templates/express/src/app/http/resources/UserResource.ts @@ -8,7 +8,7 @@ export class UserResource extends Resource { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/express/src/app/models/PasswordReset.ts b/templates/express/src/app/models/PasswordReset.ts index d7cf2fb7..a83961a1 100644 --- a/templates/express/src/app/models/PasswordReset.ts +++ b/templates/express/src/app/models/PasswordReset.ts @@ -1,29 +1,31 @@ import { Model } from 'arkormx' export class PasswordReset extends Model { - declare id: string - declare email: string | null - declare phone: string | null - declare token: string - declare createdAt: Date - declare updatedAt: Date + declare id: string + declare email: string | null + declare phone: string | null + declare token: string + declare createdAt: Date + declare updatedAt: Date - static fromToken (token: string) { - if (token.length > 6) { - token = Buffer.from(token, 'base64url').toString().split('-')[0] - } - - return this.query().where({ token }).firstOrFail() + static fromToken(token: string) { + if (token.length > 6) { + token = Buffer.from(token, 'base64url').toString().split('-')[0] } - /** - * get the password reset link - */ - link () { - const b64Token = Buffer - .from(str(this.token).append('-' + Math.random()).toString()) - .toString('base64url') + return this.query().where({ token }).firstOrFail() + } - return `${config('app.frontend_url')}/auth/reset-password?token=${b64Token}` - } + /** + * get the password reset link + */ + link() { + const b64Token = Buffer.from( + str(this.token) + .append('-' + Math.random()) + .toString(), + ).toString('base64url') + + return `${config('app.frontend_url')}/auth/reset-password?token=${b64Token}` + } } diff --git a/templates/express/src/app/models/PersonalAccessToken.ts b/templates/express/src/app/models/PersonalAccessToken.ts index 89396bac..48f9e60e 100644 --- a/templates/express/src/app/models/PersonalAccessToken.ts +++ b/templates/express/src/app/models/PersonalAccessToken.ts @@ -2,25 +2,25 @@ import { PersonalAccessToken as BasePersonalAccessToken } from '@arkstack/auth' import { User } from './User' export class PersonalAccessToken extends BasePersonalAccessToken { - declare name: string - declare token: string - declare abilities: string[] - declare deviceInfo: Record - declare lastUsedAt: Date - declare expiresAt: Date - declare createdAt: Date - declare updatedAt: Date + declare name: string + declare token: string + declare abilities: string[] + declare deviceInfo: Record + declare lastUsedAt: Date + declare expiresAt: Date + declare createdAt: Date + declare updatedAt: Date - protected static columns = { - userId: 'user_id', - deviceInfo: 'device_info', - lastUsedAt: 'last_used_at', - expiresAt: 'expires_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + deviceInfo: 'device_info', + lastUsedAt: 'last_used_at', + expiresAt: 'expires_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId') - } + user() { + return this.belongsTo(User, 'userId') + } } diff --git a/templates/express/src/app/models/User.ts b/templates/express/src/app/models/User.ts index bb4e58d5..bf3b144c 100644 --- a/templates/express/src/app/models/User.ts +++ b/templates/express/src/app/models/User.ts @@ -5,32 +5,30 @@ import { UserTwoFactor } from './UserTwoFactor' import { UserFactory } from 'src/database/factories/UserFactory' export class User extends BaseUser { - declare email: string - declare password: string - declare name: string - declare createdAt: Date - declare updatedAt: Date + declare email: string + declare password: string + declare name: string + declare createdAt: Date + declare updatedAt: Date - protected static override factoryClass = UserFactory + protected static override factoryClass = UserFactory - protected static override columns = { - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static override columns = { + createdAt: 'created_at', + updatedAt: 'updated_at', + } - protected override hidden = [ - 'password' - ] + protected override hidden = ['password'] - personalAccessTokens () { - return this.hasMany(PersonalAccessToken, 'userId') - } + personalAccessTokens() { + return this.hasMany(PersonalAccessToken, 'userId') + } - twoFactor () { - return this.hasOne(UserTwoFactor, 'userId') - } + twoFactor() { + return this.hasOne(UserTwoFactor, 'userId') + } - notifications () { - return this.hasMany(UserNotification, 'userId') - } + notifications() { + return this.hasMany(UserNotification, 'userId') + } } diff --git a/templates/express/src/app/models/UserNotification.ts b/templates/express/src/app/models/UserNotification.ts index bc04dd71..a9481b41 100644 --- a/templates/express/src/app/models/UserNotification.ts +++ b/templates/express/src/app/models/UserNotification.ts @@ -2,16 +2,16 @@ import { UserNotification as BaseUserNotification } from '@arkstack/notification import { User } from './User' export class UserNotification extends BaseUserNotification { - protected static columns = { - userId: 'user_id', - actionText: 'action_text', - actionLink: 'action_link', - readAt: 'read_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + actionText: 'action_text', + actionLink: 'action_link', + readAt: 'read_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } + user() { + return this.belongsTo(User, 'userId', 'id') + } } diff --git a/templates/express/src/app/models/UserTwoFactor.ts b/templates/express/src/app/models/UserTwoFactor.ts index 6bbc87a8..8186c98a 100644 --- a/templates/express/src/app/models/UserTwoFactor.ts +++ b/templates/express/src/app/models/UserTwoFactor.ts @@ -2,19 +2,19 @@ import { UserTwoFactor as BaseUserTwoFactor } from '@arkstack/auth' import { User } from './User' export class UserTwoFactor extends BaseUserTwoFactor { - protected static columns = { - userId: 'user_id', - secretCiphertext: 'secret_ciphertext', - smsCodeHash: 'sms_code_hash', - smsCodeExpiresAt: 'sms_code_expires_at', - smsCodePurpose: 'sms_code_purpose', - enabledAt: 'enabled_at', - recoveryCodeHashes: 'recovery_code_hashes', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + secretCiphertext: 'secret_ciphertext', + smsCodeHash: 'sms_code_hash', + smsCodeExpiresAt: 'sms_code_expires_at', + smsCodePurpose: 'sms_code_purpose', + enabledAt: 'enabled_at', + recoveryCodeHashes: 'recovery_code_hashes', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } + user() { + return this.belongsTo(User, 'userId', 'id') + } } diff --git a/templates/express/src/config/app.ts b/templates/express/src/config/app.ts index 6711600a..f8cf46f0 100644 --- a/templates/express/src/config/app.ts +++ b/templates/express/src/config/app.ts @@ -1,17 +1,17 @@ import { AppConfig } from '@arkstack/common' export default (): AppConfig => { - return { - env: env('APP_ENV', 'local'), - key: env('APP_KEY', 'change-me'), - url: env('APP_URL', 'http://localhost'), - host: env('APP_HOST', '0.0.0.0'), - name: env('APP_NAME', 'Arkstack'), - frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), - debug: env('APP_DEBUG', false), - timezone: env('APP_TIMEZONE', 'UTC'), - locale: env('APP_LOCALE', 'en'), - fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), - faker_locale: env('APP_FAKER_LOCALE', 'en_US'), - } + return { + env: env('APP_ENV', 'local'), + key: env('APP_KEY', 'change-me'), + url: env('APP_URL', 'http://localhost'), + host: env('APP_HOST', '0.0.0.0'), + name: env('APP_NAME', 'Arkstack'), + frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), + debug: env('APP_DEBUG', false), + timezone: env('APP_TIMEZONE', 'UTC'), + locale: env('APP_LOCALE', 'en'), + fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), + faker_locale: env('APP_FAKER_LOCALE', 'en_US'), + } } diff --git a/templates/express/src/config/cache.ts b/templates/express/src/config/cache.ts index 4e632b05..0787ab2f 100644 --- a/templates/express/src/config/cache.ts +++ b/templates/express/src/config/cache.ts @@ -3,48 +3,48 @@ import type { CacheConfig } from '@arkstack/cache' import path from 'node:path' export default (): CacheConfig => { - return { - /** - * Default Cache Store - * - * The store used by the framework whenever a store is not explicitly - * requested. Supported drivers: memory, file, redis, database. - */ - default: env('CACHE_STORE', 'memory'), + return { + /** + * Default Cache Store + * + * The store used by the framework whenever a store is not explicitly + * requested. Supported drivers: memory, file, redis, database. + */ + default: env('CACHE_STORE', 'memory'), - /** - * Cache Key Prefix - * - * Prepended to every cache key to avoid collisions between applications - * that share the same backing store. - */ - prefix: env('CACHE_PREFIX', 'arkstack_cache_'), + /** + * Cache Key Prefix + * + * Prepended to every cache key to avoid collisions between applications + * that share the same backing store. + */ + prefix: env('CACHE_PREFIX', 'arkstack_cache_'), - /** - * Cache Stores - * - * Configure as many stores as you like. Each is referenced by name via - * `Cache.store('')`. - */ - stores: { - memory: { - driver: 'memory', - }, - file: { - driver: 'file', - path: path.join(Arkstack.rootDir(), './storage/framework/cache'), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_CACHE_DB', 1), - }, - database: { - driver: 'database', - table: env('CACHE_TABLE', 'cache'), - }, - }, - } + /** + * Cache Stores + * + * Configure as many stores as you like. Each is referenced by name via + * `Cache.store('')`. + */ + stores: { + memory: { + driver: 'memory', + }, + file: { + driver: 'file', + path: path.join(Arkstack.rootDir(), './storage/framework/cache'), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_CACHE_DB', 1), + }, + database: { + driver: 'database', + table: env('CACHE_TABLE', 'cache'), + }, + }, + } } diff --git a/templates/express/src/config/cors.ts b/templates/express/src/config/cors.ts index 7fb44d86..17ed2515 100644 --- a/templates/express/src/config/cors.ts +++ b/templates/express/src/config/cors.ts @@ -1,13 +1,15 @@ import appConfig from './app' export default () => { - const app = appConfig() + const app = appConfig() - return { - allowed_origins: [ - app.url, - app.frontend_url, - ...env('CORS_ALLOWED_ORIGINS', '').split(',').map((origin: string) => origin.trim()) - ].filter(Boolean) - } -} \ No newline at end of file + return { + allowed_origins: [ + app.url, + app.frontend_url, + ...env('CORS_ALLOWED_ORIGINS', '') + .split(',') + .map((origin: string) => origin.trim()), + ].filter(Boolean), + } +} diff --git a/templates/express/src/config/database.ts b/templates/express/src/config/database.ts index a316bbd4..46c7f535 100644 --- a/templates/express/src/config/database.ts +++ b/templates/express/src/config/database.ts @@ -1,31 +1,31 @@ import { DatabaseConfig } from '@arkstack/database' export default (): DatabaseConfig => { - return { - /** - * Default Database Connection - * - * The connection used whenever one is not explicitly requested. - * Supported drivers: postgres. - */ - default: env('DB_CONNECTION', 'pgsql'), + return { + /** + * Default Database Connection + * + * The connection used whenever one is not explicitly requested. + * Supported drivers: postgres. + */ + default: env('DB_CONNECTION', 'pgsql'), - /** - * Database Connections - * - * Credentials are provided as discrete fields. When `url` (or - * `DATABASE_URL`) is set it takes precedence over the discrete fields. - */ - connections: { - pgsql: { - driver: 'postgres', - url: env('DATABASE_URL'), - host: env('DB_HOST', '127.0.0.1'), - port: env('DB_PORT', 5432), - user: env('DB_USERNAME', 'postgres'), - database: env('DB_DATABASE', 'arkstack'), - password: env('DB_PASSWORD', 'postgres'), - }, - }, - } + /** + * Database Connections + * + * Credentials are provided as discrete fields. When `url` (or + * `DATABASE_URL`) is set it takes precedence over the discrete fields. + */ + connections: { + pgsql: { + driver: 'postgres', + url: env('DATABASE_URL'), + host: env('DB_HOST', '127.0.0.1'), + port: env('DB_PORT', 5432), + user: env('DB_USERNAME', 'postgres'), + database: env('DB_DATABASE', 'arkstack'), + password: env('DB_PASSWORD', 'postgres'), + }, + }, + } } diff --git a/templates/express/src/config/filesystem.ts b/templates/express/src/config/filesystem.ts index 849f2f34..ac637d79 100644 --- a/templates/express/src/config/filesystem.ts +++ b/templates/express/src/config/filesystem.ts @@ -3,84 +3,84 @@ import { FilesystemConfig } from '@arkstack/filesystem' import path from 'node:path' export default (): FilesystemConfig => { - return { - /** - * Default filesystem disk to be used by the framework. - */ - default: env('FILESYSTEM_DISK', 'local'), + return { + /** + * Default filesystem disk to be used by the framework. + */ + default: env('FILESYSTEM_DISK', 'local'), - /** - * Available disks and their configurations. - * - * You can configure as many disks as you want, and even have multiple disks - * using the same driver. - * - * Supported drivers: local, ftp, s3 - */ - disks: { - local: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app'), - visibility: 'public', - }, - public: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app/public'), - visibility: 'public' - }, - ftp: { - driver: 'ftp', - host: env('FTP_HOST'), - username: env('FTP_USERNAME'), - password: env('FTP_PASSWORD'), - port: env('FTP_PORT', 21), - verbose: env('FTP_VERBOSE', false), - privateKey: env('FTP_PRIVATE_KEY'), - }, - s3: { - driver: 's3', - key: env('AWS_ACCESS_KEY_ID'), - secret: env('AWS_SECRET_ACCESS_KEY'), - region: env('AWS_DEFAULT_REGION'), - bucket: env('AWS_BUCKET'), - url: env('AWS_URL'), - endpoint: env('AWS_ENDPOINT'), - visibility: 'public', - }, - gcs: { - driver: 'gcs', - projectId: env('GOOGLE_CLOUD_PROJECT'), - keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), - bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), - visibility: 'private', - usingUniformAcl: true, - } - }, + /** + * Available disks and their configurations. + * + * You can configure as many disks as you want, and even have multiple disks + * using the same driver. + * + * Supported drivers: local, ftp, s3 + */ + disks: { + local: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app'), + visibility: 'public', + }, + public: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app/public'), + visibility: 'public', + }, + ftp: { + driver: 'ftp', + host: env('FTP_HOST'), + username: env('FTP_USERNAME'), + password: env('FTP_PASSWORD'), + port: env('FTP_PORT', 21), + verbose: env('FTP_VERBOSE', false), + privateKey: env('FTP_PRIVATE_KEY'), + }, + s3: { + driver: 's3', + key: env('AWS_ACCESS_KEY_ID'), + secret: env('AWS_SECRET_ACCESS_KEY'), + region: env('AWS_DEFAULT_REGION'), + bucket: env('AWS_BUCKET'), + url: env('AWS_URL'), + endpoint: env('AWS_ENDPOINT'), + visibility: 'public', + }, + gcs: { + driver: 'gcs', + projectId: env('GOOGLE_CLOUD_PROJECT'), + keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), + bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), + visibility: 'private', + usingUniformAcl: true, + }, + }, - /** - * Optional symbolic links to create when the `storage:link` command is executed. - * The key of the object represents the link location, while the value represents the target location. - */ - links: { - [path.join(Arkstack.rootDir(), './public/storage')]: path.join(Arkstack.rootDir(), './storage/app/public'), - }, + /** + * Optional symbolic links to create when the `storage:link` command is executed. + * The key of the object represents the link location, while the value represents the target location. + */ + links: { + [path.join(Arkstack.rootDir(), './public/storage')]: path.join(Arkstack.rootDir(), './storage/app/public'), + }, - /** - * Optional file name generator function. - * If provided, this function will be used to generate unique file names - * for uploaded files when using the `saveFile` method of the Storage class. - * - * @param originalName The original name of the file - * @returns A unique file name - * - * fileNameGenerator: (originalName: string) => { - * const timestamp = Date.now() - * const randomString = Math.random().toString(36).substring(2, 8) - * const extension = path.extname(originalName) - * const baseName = path.basename(originalName, extension) - * - * return `${baseName}-${timestamp}-${randomString}${extension}` - * }, - */ - } + /** + * Optional file name generator function. + * If provided, this function will be used to generate unique file names + * for uploaded files when using the `saveFile` method of the Storage class. + * + * @param originalName The original name of the file + * @returns A unique file name + * + * fileNameGenerator: (originalName: string) => { + * const timestamp = Date.now() + * const randomString = Math.random().toString(36).substring(2, 8) + * const extension = path.extname(originalName) + * const baseName = path.basename(originalName, extension) + * + * return `${baseName}-${timestamp}-${randomString}${extension}` + * }, + */ + } } diff --git a/templates/express/src/config/middleware.ts b/templates/express/src/config/middleware.ts index a49bd7a8..e46113f6 100644 --- a/templates/express/src/config/middleware.ts +++ b/templates/express/src/config/middleware.ts @@ -29,10 +29,8 @@ export default (): MiddlewareConfig => { (req, _res, next) => { useExpressUploadContext(req as never) next() - } - ], - after: [ - requestLogger() + }, ], + after: [requestLogger()], } -} +} diff --git a/templates/express/src/config/notifications.ts b/templates/express/src/config/notifications.ts index 5777b6b0..13cf2e4e 100644 --- a/templates/express/src/config/notifications.ts +++ b/templates/express/src/config/notifications.ts @@ -3,164 +3,164 @@ import { NotificationConfig } from '@arkstack/notifications' import { join } from 'node:path' export default (): NotificationConfig => { - return { + return { + /** + * Default Notification Driver + * + * The default notifications driver that is utilized for outgoing messages. + */ + default_driver: env('NOTIFICATION_DRIVER', 'mail'), + + /** + * Notification Drivers + * + * Notifications will be sent using one of the configured drivers. + */ + drivers: { + mail: { + transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', + from: { + name: env('MAIL_FROM_NAME', 'Arcstack'), + address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), + }, + test_address: env('MAIL_TEST_ADDRESS'), + }, + sms: { + transport: 'africastalking', + from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), + }, + db: { + table: 'user_notifications', + }, + realtime: { /** - * Default Notification Driver - * - * The default notifications driver that is utilized for outgoing messages. + * pusher | firebase | firebase-database | polling | memory */ - default_driver: env('NOTIFICATION_DRIVER', 'mail'), - + transport: env('REALTIME_TRANSPORT', 'pusher'), /** - * Notification Drivers - * - * Notifications will be sent using one of the configured drivers. + * Also push user notifications to their devices: 'firebase' or false. */ - drivers: { - mail: { - transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', - from: { - name: env('MAIL_FROM_NAME', 'Arcstack'), - address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), - }, - test_address: env('MAIL_TEST_ADDRESS'), - }, - sms: { - transport: 'africastalking', - from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), - }, - db: { - table: 'user_notifications', - }, - realtime: { - /** - * pusher | firebase | firebase-database | polling | memory - */ - transport: env('REALTIME_TRANSPORT', 'pusher'), - /** - * Also push user notifications to their devices: 'firebase' or false. - */ - push: env('REALTIME_PUSH', false), - channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), - event: env('REALTIME_EVENT', 'notification'), - store: env('REALTIME_STORE', false), - }, + push: env('REALTIME_PUSH', false), + channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), + event: env('REALTIME_EVENT', 'notification'), + store: env('REALTIME_STORE', false), + }, + }, + transports: { + /** + * SMTP Notifications Transport + * + * Outgoing notifications will be sent as mail using SMTP. + */ + smtp: { + host: env('MAIL_HOST', 'localhost'), + port: env('MAIL_PORT', 1025), + secure: env('MAIL_SECURE', false), + auth: { + user: env('MAIL_USERNAME', 'user@example.com'), + pass: env('MAIL_PASSWORD', 'password'), }, - transports: { - /** - * SMTP Notifications Transport - * - * Outgoing notifications will be sent as mail using SMTP. - */ - smtp: { - host: env('MAIL_HOST', 'localhost'), - port: env('MAIL_PORT', 1025), - secure: env('MAIL_SECURE', false), - auth: { - user: env('MAIL_USERNAME', 'user@example.com'), - pass: env('MAIL_PASSWORD', 'password'), - }, - }, + }, - /** - * File Notifications Transport - * - * Outgoing notifications will be sent as mail but stored locally in a file - * for testing purposes. - */ - file: { - /** - * Mails Directory - * - * Outgoing mails will be intercepted and stored in this directory. - */ - directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), - }, + /** + * File Notifications Transport + * + * Outgoing notifications will be sent as mail but stored locally in a file + * for testing purposes. + */ + file: { + /** + * Mails Directory + * + * Outgoing mails will be intercepted and stored in this directory. + */ + directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), + }, - /** - * Africa's Talking Notifications Transport - * - * Outgoing notifications will be sent as SMS via Africa's Talking. - */ - africastalking: { - username: env('AFRICASTALKING_USERNAME', 'sandbox'), - apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), - senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), - }, + /** + * Africa's Talking Notifications Transport + * + * Outgoing notifications will be sent as SMS via Africa's Talking. + */ + africastalking: { + username: env('AFRICASTALKING_USERNAME', 'sandbox'), + apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), + senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), + }, - /** - * Twilio Notifications Transport - * - * Outgoing notifications will be sent as SMS via Twilio. - */ - twilio: { - accountSid: env('TWILIO_ACCOUNT_SID'), - authToken: env('TWILIO_AUTH_TOKEN'), - from: env('TWILIO_FROM', env('SMS_FROM')), - }, + /** + * Twilio Notifications Transport + * + * Outgoing notifications will be sent as SMS via Twilio. + */ + twilio: { + accountSid: env('TWILIO_ACCOUNT_SID'), + authToken: env('TWILIO_AUTH_TOKEN'), + from: env('TWILIO_FROM', env('SMS_FROM')), + }, - /** - * Pusher Realtime Transport - * - * Realtime notifications will be broadcast over Pusher Channels. - */ - pusher: { - app_id: env('PUSHER_APP_ID', ''), - key: env('PUSHER_KEY', ''), - secret: env('PUSHER_SECRET', ''), - cluster: env('PUSHER_CLUSTER', 'mt1'), - use_tls: env('PUSHER_USE_TLS', true), - // Set to use a self-hosted Pusher-protocol server such as soketi. - host: env('PUSHER_HOST', undefined), - port: env('PUSHER_PORT', undefined), - }, + /** + * Pusher Realtime Transport + * + * Realtime notifications will be broadcast over Pusher Channels. + */ + pusher: { + app_id: env('PUSHER_APP_ID', ''), + key: env('PUSHER_KEY', ''), + secret: env('PUSHER_SECRET', ''), + cluster: env('PUSHER_CLUSTER', 'mt1'), + use_tls: env('PUSHER_USE_TLS', true), + // Set to use a self-hosted Pusher-protocol server such as soketi. + host: env('PUSHER_HOST', undefined), + port: env('PUSHER_PORT', undefined), + }, - /** - * Firebase Realtime Transport - * - * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. - */ - firebase: { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - web: { - api_key: env('FIREBASE_API_KEY', ''), - app_id: env('FIREBASE_APP_ID', ''), - messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), - vapid_key: env('FIREBASE_VAPID_KEY', ''), - }, - }, + /** + * Firebase Realtime Transport + * + * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. + */ + firebase: { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + web: { + api_key: env('FIREBASE_API_KEY', ''), + app_id: env('FIREBASE_APP_ID', ''), + messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), + vapid_key: env('FIREBASE_VAPID_KEY', ''), + }, + }, - /** - * Firebase Realtime Database Transport - * - * Realtime notifications will be broadcast over Realtime Database channels. - */ - 'firebase-database': { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - database_url: env('FIREBASE_DATABASE_URL', ''), - web: { - api_key: env('FIREBASE_API_KEY', ''), - auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), - app_id: env('FIREBASE_APP_ID', ''), - }, - }, + /** + * Firebase Realtime Database Transport + * + * Realtime notifications will be broadcast over Realtime Database channels. + */ + 'firebase-database': { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + database_url: env('FIREBASE_DATABASE_URL', ''), + web: { + api_key: env('FIREBASE_API_KEY', ''), + auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), + app_id: env('FIREBASE_APP_ID', ''), + }, + }, - /** - * Polling Realtime Transport - * - * Nothing is pushed; clients poll on this interval (ms) instead. - */ - polling: { - interval: env('REALTIME_POLL_INTERVAL', 5000), - }, - } - } + /** + * Polling Realtime Transport + * + * Nothing is pushed; clients poll on this interval (ms) instead. + */ + polling: { + interval: env('REALTIME_POLL_INTERVAL', 5000), + }, + }, + } } diff --git a/templates/express/src/config/queue.ts b/templates/express/src/config/queue.ts index f1bd0566..68cf03fe 100644 --- a/templates/express/src/config/queue.ts +++ b/templates/express/src/config/queue.ts @@ -1,43 +1,43 @@ import type { QueueConfig } from '@arkstack/queue' export default (): QueueConfig => { - return { - /** - * Default Queue Connection - * - * The connection used when a job is dispatched without specifying one. - * The `sync` connection runs jobs immediately (no worker required) and is - * a sensible default for local development and tests. - * - * Supported drivers: sync, database, redis. - */ - default: env('QUEUE_CONNECTION', 'sync'), + return { + /** + * Default Queue Connection + * + * The connection used when a job is dispatched without specifying one. + * The `sync` connection runs jobs immediately (no worker required) and is + * a sensible default for local development and tests. + * + * Supported drivers: sync, database, redis. + */ + default: env('QUEUE_CONNECTION', 'sync'), - /** - * Queue Connections - * - * Configure a connection per backend. Run a worker with `ark queue:work` - * to process jobs on the database and redis connections. - */ - connections: { - sync: { - driver: 'sync', - }, - database: { - driver: 'database', - table: env('QUEUE_TABLE', 'jobs'), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_QUEUE_DB', 0), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - }, - } + /** + * Queue Connections + * + * Configure a connection per backend. Run a worker with `ark queue:work` + * to process jobs on the database and redis connections. + */ + connections: { + sync: { + driver: 'sync', + }, + database: { + driver: 'database', + table: env('QUEUE_TABLE', 'jobs'), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_QUEUE_DB', 0), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + }, + } } diff --git a/templates/express/src/config/resources.ts b/templates/express/src/config/resources.ts index 0561b2b9..3ab17a98 100644 --- a/templates/express/src/config/resources.ts +++ b/templates/express/src/config/resources.ts @@ -9,44 +9,44 @@ import type { ResoraConfig } from 'resora' * resora accepts at runtime but doesn't surface in its narrower config type. */ export default (): ResoraConfig => { - return { - preferredCase: 'camel', - responseStructure: { - wrap: true, - rootKey: 'data', - }, - paginatedExtras: ['meta', 'links'], - baseUrl: env('APP_URL', 'http://localhost'), - pageName: 'page', - paginatedLinks: { - first: 'first', - last: 'last', - prev: 'prev', - next: 'next', - }, - paginatedMeta: { - to: 'to', - from: 'from', - links: 'links', - path: 'path', - total: 'total', - per_page: 'per_page', - last_page: 'last_page', - current_page: 'current_page', - }, - cursorMeta: { - previous: 'previous', - next: 'next', - }, - resourcesDir: 'src/app/http/resources', - localStubsDir: 'node_modules/@arkstack/driver-express/stubs', - stubs: { - resource: 'resource.stub', - collection: 'resource.collection.stub', - controller: 'controller.stub', - api: 'controller.api.stub', - model: 'controller.model.stub', - apiResource: 'controller.api.resource.stub', - }, - } as ResoraConfig + return { + preferredCase: 'camel', + responseStructure: { + wrap: true, + rootKey: 'data', + }, + paginatedExtras: ['meta', 'links'], + baseUrl: env('APP_URL', 'http://localhost'), + pageName: 'page', + paginatedLinks: { + first: 'first', + last: 'last', + prev: 'prev', + next: 'next', + }, + paginatedMeta: { + to: 'to', + from: 'from', + links: 'links', + path: 'path', + total: 'total', + per_page: 'per_page', + last_page: 'last_page', + current_page: 'current_page', + }, + cursorMeta: { + previous: 'previous', + next: 'next', + }, + resourcesDir: 'src/app/http/resources', + localStubsDir: 'node_modules/@arkstack/driver-express/stubs', + stubs: { + resource: 'resource.stub', + collection: 'resource.collection.stub', + controller: 'controller.stub', + api: 'controller.api.stub', + model: 'controller.model.stub', + apiResource: 'controller.api.resource.stub', + }, + } as ResoraConfig } diff --git a/templates/express/src/config/session.ts b/templates/express/src/config/session.ts index 0a829e18..1f5fa62d 100644 --- a/templates/express/src/config/session.ts +++ b/templates/express/src/config/session.ts @@ -4,83 +4,80 @@ import { Str } from '@h3ravel/support' import { join } from 'node:path' export default (): SessionConfig => { - return { - secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), + return { + secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), - /** - * Session Driver - * - * The default session driver that is utilized for incoming requests. - * - * Supported: "file", "cookie", "database", - * (WIP: "apc", "memcached", "redis", "dynamodb", "array") - */ - driver: env('SESSION_DRIVER', 'file'), + /** + * Session Driver + * + * The default session driver that is utilized for incoming requests. + * + * Supported: "file", "cookie", "database", + * (WIP: "apc", "memcached", "redis", "dynamodb", "array") + */ + driver: env('SESSION_DRIVER', 'file'), - /** - * Cookie Name - * - * The name of the session cookie that is created by Arkstack. - */ - cookie: env( - 'SESSION_COOKIE', - Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session' - ), + /** + * Cookie Name + * + * The name of the session cookie that is created by Arkstack. + */ + cookie: env('SESSION_COOKIE', Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session'), - /** - * Session Time To Live - * - * The number of minutes that the session is allowed to remain idle before - * it expires. - */ - ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), + /** + * Session Time To Live + * + * The number of minutes that the session is allowed to remain idle before + * it expires. + */ + ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), - /** - * HTTP Only Access - * - * Prevent JavaScript from accessing the value of the cookie making it - * only accessible through the HTTP protocol. - */ - http_only: true, + /** + * HTTP Only Access + * + * Prevent JavaScript from accessing the value of the cookie making it + * only accessible through the HTTP protocol. + */ + http_only: true, - /** - * HTTPS Only Access - * - * This option ensures that cookies will only be sent back to the server - * if the browser has a HTTPS connection if set to "true". - */ - secure: env('NODE_ENV', 'development') === 'production', + /** + * HTTPS Only Access + * + * This option ensures that cookies will only be sent back to the server + * if the browser has a HTTPS connection if set to "true". + */ + secure: env('NODE_ENV', 'development') === 'production', - /** - * Same-Site Cookies - * - * Determines how cookies behave when cross-site requests take - * place, and can be used to mitigate CSRF attacks. - * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - * - * Supported: "Lax", "Strict", "None", undefined - */ - same_site: 'Lax', + /** + * Same-Site Cookies + * + * Determines how cookies behave when cross-site requests take + * place, and can be used to mitigate CSRF attacks. + * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + * + * Supported: "Lax", "Strict", "None", undefined + */ + same_site: 'Lax', - /** - * Session Cookie Path - * - * Determines the path for which the cookie will be available. - */ - path: '/', - /** - * Datbase Table - * - * For the "database" session driver, this will determine the table to - * be used to store sessions. - */ - table: env('SESSION_TABLE', 'sessions'), + /** + * Session Cookie Path + * + * Determines the path for which the cookie will be available. + */ + path: '/', + /** + * Datbase Table + * + * For the "database" session driver, this will determine the table to + * be used to store sessions. + */ + table: env('SESSION_TABLE', 'sessions'), - /** - * Session Directory - * - * For the "file" session driver, the session files are placed in this directory. - */ - directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), - } + /** + * Session Directory + * + * For the "file" session driver, the session files are placed in this directory. + */ + directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), + } } diff --git a/templates/express/src/core/app.ts b/templates/express/src/core/app.ts index fbd9315a..8129b712 100644 --- a/templates/express/src/core/app.ts +++ b/templates/express/src/core/app.ts @@ -6,11 +6,11 @@ import { type Express, type Handler } from 'express' export default class Application extends Arkstack { /** - * Creates an instance of the Application class, initializing - * the Express driver with the provided options and creating an Express + * Creates an instance of the Application class, initializing + * the Express driver with the provided options and creating an Express * application instance. - * - * @param app + * + * @param app */ constructor(app?: Express) { super() @@ -28,10 +28,10 @@ export default class Application extends Arkstack { /** * Gets the ArkstackRouterContract implementation for the Express framework. - * - * @returns + * + * @returns */ - getRouter (): ArkstackRouterContract { + getRouter(): ArkstackRouterContract { return { bind: (_app: Express) => Router.bind(), list: (options: ArkstackRouteListOptions = {}) => Router.list(options), @@ -39,20 +39,20 @@ export default class Application extends Arkstack { } /** - * Boots the application by mounting public assets, binding the + * Boots the application by mounting public assets, binding the * router, applying middleware, and starting the server. - * + * * @param port The numeric port to run the server on * @param defer Set to true to skip server startup */ - public async boot (port: number, defer = false) { + public async boot(port: number, defer = false) { // Load public assets await this.driver.mountPublicAssets(this.app, path.join(Arkstack.rootDir(), 'public')) // Apply all middleware await this.driver.applyMiddleware(this.app, config('middleware') as any) - // Bind the router + // Bind the router await this.driver.bindRouter(this.app) // Error Handler diff --git a/templates/express/src/database/factories/UserFactory.ts b/templates/express/src/database/factories/UserFactory.ts index ce2528af..7076c8b8 100644 --- a/templates/express/src/database/factories/UserFactory.ts +++ b/templates/express/src/database/factories/UserFactory.ts @@ -4,13 +4,13 @@ import { User } from '@app/models/User' import { faker } from '@faker-js/faker' export class UserFactory extends ModelFactory { - protected model = User + protected model = User - protected definition (_sequence: number) { - return { - 'name': faker.person.fullName(), - 'email': faker.internet.email(), - 'password': Hash.make('password'), - } + protected definition(_sequence: number) { + return { + name: faker.person.fullName(), + email: faker.internet.email(), + password: Hash.make('password'), } + } } diff --git a/templates/express/src/database/migrations/20260428164120_create_users_table.ts b/templates/express/src/database/migrations/20260428164120_create_users_table.ts index 8c6f52ac..cb489074 100644 --- a/templates/express/src/database/migrations/20260428164120_create_users_table.ts +++ b/templates/express/src/database/migrations/20260428164120_create_users_table.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUsersTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('users', (table) => { - table.id('id', 'uuid').primary() - table.string('name') - table.string('email').unique().index() - table.string('password') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("users", (table) => { + table.id("id", "uuid").primary(); + table.string("name"); + table.string("email").unique().index(); + table.string("password"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('users') - } -} \ No newline at end of file + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("users"); + } +} diff --git a/templates/express/src/database/migrations/20260428164833_createpasswordresetstable.ts b/templates/express/src/database/migrations/20260428164833_createpasswordresetstable.ts index 9315d10b..f6dc4083 100644 --- a/templates/express/src/database/migrations/20260428164833_createpasswordresetstable.ts +++ b/templates/express/src/database/migrations/20260428164833_createpasswordresetstable.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePasswordResetsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('password_resets', (table) => { - table.id('id', 'uuid').primary() - table.string('email').nullable().index() - table.string('phone').nullable().index() - table.string('token') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("password_resets", (table) => { + table.id("id", "uuid").primary(); + table.string("email").nullable().index(); + table.string("phone").nullable().index(); + table.string("token"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('password_resets') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("password_resets"); + } } diff --git a/templates/express/src/database/migrations/20260428165239_create_person_access_tokens_table.ts b/templates/express/src/database/migrations/20260428165239_create_person_access_tokens_table.ts index 252d0f47..45e1249b 100644 --- a/templates/express/src/database/migrations/20260428165239_create_person_access_tokens_table.ts +++ b/templates/express/src/database/migrations/20260428165239_create_person_access_tokens_table.ts @@ -1,23 +1,29 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePersonAccessTokensTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('personal_access_tokens', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('personalAccessTokens') - table.string('name') - table.text('token').unique() - table.json('abilities').nullable() - table.json('deviceInfo').nullable().map('device_info') - table.date('lastUsedAt').nullable().map('last_used_at') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("personal_access_tokens", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("personalAccessTokens"); + table.string("name"); + table.text("token").unique(); + table.json("abilities").nullable(); + table.json("deviceInfo").nullable().map("device_info"); + table.date("lastUsedAt").nullable().map("last_used_at"); + table.date("expiresAt").nullable().map("expires_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('personal_access_tokens') - } -} + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("personal_access_tokens"); + } +} diff --git a/templates/express/src/database/migrations/20260505110000_create_user_two_factors_table.ts b/templates/express/src/database/migrations/20260505110000_create_user_two_factors_table.ts index 7542293b..1647dda3 100644 --- a/templates/express/src/database/migrations/20260505110000_create_user_two_factors_table.ts +++ b/templates/express/src/database/migrations/20260505110000_create_user_two_factors_table.ts @@ -1,24 +1,33 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserTwoFactorsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_two_factors', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('twoFactor') - table.enum('method', ['authenticator', 'sms']).nullable().enumName('TwoFactorMethod') - table.string('secretCiphertext').nullable().map('secret_ciphertext') - table.string('smsCodeHash').nullable().map('sms_code_hash') - table.date('smsCodeExpiresAt').nullable().map('sms_code_expires_at') - table.string('smsCodePurpose').nullable().map('sms_code_purpose') - table.date('enabledAt').nullable().map('enabled_at') - table.json('recoveryCodeHashes').nullable().map('recovery_code_hashes') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_two_factors", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("twoFactor"); + table + .enum("method", ["authenticator", "sms"]) + .nullable() + .enumName("TwoFactorMethod"); + table.string("secretCiphertext").nullable().map("secret_ciphertext"); + table.string("smsCodeHash").nullable().map("sms_code_hash"); + table.date("smsCodeExpiresAt").nullable().map("sms_code_expires_at"); + table.string("smsCodePurpose").nullable().map("sms_code_purpose"); + table.date("enabledAt").nullable().map("enabled_at"); + table.json("recoveryCodeHashes").nullable().map("recovery_code_hashes"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_two_factors') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_two_factors"); + } } diff --git a/templates/express/src/database/migrations/20260505111000_create_user_notifications_table.ts b/templates/express/src/database/migrations/20260505111000_create_user_notifications_table.ts index 7690899b..8234a972 100644 --- a/templates/express/src/database/migrations/20260505111000_create_user_notifications_table.ts +++ b/templates/express/src/database/migrations/20260505111000_create_user_notifications_table.ts @@ -1,24 +1,30 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserNotificationsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_notifications', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('notifications') - table.string('type').nullable() - table.string('title') - table.text('description') - table.string('actionText').nullable().map('action_text') - table.string('actionLink').nullable().map('action_link') - table.json('meta').nullable() - table.date('readAt').nullable().map('read_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_notifications", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("notifications"); + table.string("type").nullable(); + table.string("title"); + table.text("description"); + table.string("actionText").nullable().map("action_text"); + table.string("actionLink").nullable().map("action_link"); + table.json("meta").nullable(); + table.date("readAt").nullable().map("read_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_notifications') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_notifications"); + } } diff --git a/templates/express/src/database/migrations/20260524000000_create_sessions_table.ts b/templates/express/src/database/migrations/20260524000000_create_sessions_table.ts index d61c2d64..e3f71385 100644 --- a/templates/express/src/database/migrations/20260524000000_create_sessions_table.ts +++ b/templates/express/src/database/migrations/20260524000000_create_sessions_table.ts @@ -1,16 +1,16 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateSessionsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('sessions', (table) => { - table.string('id').primary() - table.text('payload') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("sessions", (table) => { + table.string("id").primary(); + table.text("payload"); + table.date("expiresAt").nullable().map("expires_at"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('sessions') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("sessions"); + } } diff --git a/templates/express/src/database/seeders/DatabaseSeeder.ts b/templates/express/src/database/seeders/DatabaseSeeder.ts index 2e3d0cb6..23079b78 100644 --- a/templates/express/src/database/seeders/DatabaseSeeder.ts +++ b/templates/express/src/database/seeders/DatabaseSeeder.ts @@ -2,9 +2,7 @@ import { Seeder } from '@arkstack/database' import { UserSeeder } from './UserSeeder' export class DatabaseSeeder extends Seeder { - public async run (): Promise { - this.call([ - UserSeeder - ]) - } + public async run(): Promise { + this.call([UserSeeder]) + } } diff --git a/templates/express/src/database/seeders/UserSeeder.ts b/templates/express/src/database/seeders/UserSeeder.ts index 66b32f16..fcfb7800 100644 --- a/templates/express/src/database/seeders/UserSeeder.ts +++ b/templates/express/src/database/seeders/UserSeeder.ts @@ -2,7 +2,7 @@ import { Seeder } from '@arkstack/database' import { User } from 'src/app/models/User' export class UserSeeder extends Seeder { - public async run (): Promise { - User.factory(5).create() - } + public async run(): Promise { + User.factory(5).create() + } } diff --git a/templates/express/src/routes/console.ts b/templates/express/src/routes/console.ts index bbb354e6..c872552f 100644 --- a/templates/express/src/routes/console.ts +++ b/templates/express/src/routes/console.ts @@ -12,5 +12,7 @@ import { Schedule } from '@arkstack/scheduler' */ // Schedule.command('cache:prune').hourly() -Schedule.call(() => console.log('tick')).everyFiveMinutes().withoutOverlapping() +Schedule.call(() => console.log('tick')) + .everyFiveMinutes() + .withoutOverlapping() // Schedule.exec('backup.sh').dailyAt('01:30').onOneServer() diff --git a/templates/express/src/routes/web.ts b/templates/express/src/routes/web.ts index b0254c26..eb2ab938 100644 --- a/templates/express/src/routes/web.ts +++ b/templates/express/src/routes/web.ts @@ -6,4 +6,4 @@ Router.get('/', async () => { title: 'Welcome to Arkstack', message: 'Server running — ready for requests', }) -}) +}) diff --git a/templates/express/tests/basic.test.ts b/templates/express/tests/basic.test.ts index 5e60252d..50d1747a 100644 --- a/templates/express/tests/basic.test.ts +++ b/templates/express/tests/basic.test.ts @@ -15,15 +15,15 @@ const rwd = path.join(process.cwd(), '/templates/express') if (existsSync(rwd)) Arkstack.setRootDir(rwd) describe('starter template', () => { - it('runs a basic assertion', () => { - expect(1 + 1).toBe(2) - }) + it('runs a basic assertion', () => { + expect(1 + 1).toBe(2) + }) - it('can test a route', async () => { - await request(app).get('/api').contains('{"status":"OK"}') - }) + it('can test a route', async () => { + await request(app).get('/api').contains('{"status":"OK"}') + }) - it('can test an api route', async () => { - await request(app).get('/api/users').contains('"status":"success","message":"OK"') - }) + it('can test an api route', async () => { + await request(app).get('/api/users').contains('"status":"success","message":"OK"') + }) }) diff --git a/templates/express/tests/decorators.test.ts b/templates/express/tests/decorators.test.ts index ef3c54c3..2cb652a0 100644 --- a/templates/express/tests/decorators.test.ts +++ b/templates/express/tests/decorators.test.ts @@ -10,39 +10,39 @@ import { Router } from 'clear-router/express' import { request } from 'parasito' describe('@Bind() — Controllers', () => { - let app: express.Application - let router: ExRouter - - const setupApp = async (): Promise => { - Router.apply(router) - app.use(router) - } - - beforeEach(() => { - Router.reset() - Container.clear() - - app = express() - router = ExRouter() - app.use(express.json()) - }) - - class UserController extends BaseController { - @Bind() - async test (req: Request) { - return req.url ?? '' - } + let app: express.Application + let router: ExRouter + + const setupApp = async (): Promise => { + Router.apply(router) + app.use(router) + } + + beforeEach(() => { + Router.reset() + Container.clear() + + app = express() + router = ExRouter() + app.use(express.json()) + }) + + class UserController extends BaseController { + @Bind() + async test(req: Request) { + return req.url ?? '' } - - it('can setup basic route', async () => { - Router.get('/test', ({ res }) => res.send('OK')) - await setupApp() - await request(app).get('/test').expect(200).expect('OK') - }) - - it('can use container injection in controllers', async () => { - Router.get('/test/:id', [UserController, 'test']) - await setupApp() - await request(app).get('/test/1541').expect(200).expect('/test/1541') - }) -}) \ No newline at end of file + } + + it('can setup basic route', async () => { + Router.get('/test', ({ res }) => res.send('OK')) + await setupApp() + await request(app).get('/test').expect(200).expect('OK') + }) + + it('can use container injection in controllers', async () => { + Router.get('/test/:id', [UserController, 'test']) + await setupApp() + await request(app).get('/test/1541').expect(200).expect('/test/1541') + }) +}) diff --git a/templates/express/tsconfig.json b/templates/express/tsconfig.json index 5bc93038..5722af5c 100644 --- a/templates/express/tsconfig.json +++ b/templates/express/tsconfig.json @@ -1,3 +1,3 @@ { "extends": "./.arkstack/tsconfig.json" -} \ No newline at end of file +} diff --git a/templates/express/tsdown.config.ts b/templates/express/tsdown.config.ts index 85a14f96..b1965105 100644 --- a/templates/express/tsdown.config.ts +++ b/templates/express/tsdown.config.ts @@ -31,7 +31,7 @@ export default defineConfig([ }), execArgv: ['-r', 'source-map-support/register'], input: path.join(Arkstack.rootDir(), 'src/server.ts'), - }) + }), ], outExtensions: () => { return { diff --git a/templates/express/vitest.config.ts b/templates/express/vitest.config.ts index b10989bd..59d953a2 100644 --- a/templates/express/vitest.config.ts +++ b/templates/express/vitest.config.ts @@ -1,21 +1,21 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ - oxc: { - decorator: { - legacy: true, - emitDecoratorMetadata: true, - } + oxc: { + decorator: { + legacy: true, + emitDecoratorMetadata: true, }, - resolve: { - tsconfigPaths: true - }, - test: { - environment: 'node', - include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - env: { - NODE_ENV: 'test', - VERBOSITY: '0' - }, + }, + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + env: { + NODE_ENV: 'test', + VERBOSITY: '0', }, + }, }) diff --git a/templates/h3/eslint.config.js b/templates/h3/eslint.config.js index cfa6a459..b7f24597 100644 --- a/templates/h3/eslint.config.js +++ b/templates/h3/eslint.config.js @@ -1,72 +1,74 @@ import { defineConfig } from 'eslint/config' +import eslintConfigPrettier from 'eslint-config-prettier' import { globalIgnores } from 'eslint/config' import js from '@eslint/js' import markdown from '@eslint/markdown' import tseslint from 'typescript-eslint' export default defineConfig( - { - languageOptions: { - globals: { - console: 'readonly', - process: 'readonly', - }, - parserOptions: { - tsconfigRootDir: process.cwd() - } - }, + { + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + }, + parserOptions: { + tsconfigRootDir: process.cwd(), + }, }, - js.configs.recommended, - ...tseslint.configs.recommended, - { - files: ['docs/**/*.md', 'README.md'], - plugins: { - markdown, - }, - extends: ['markdown/recommended'], - rules: { - 'no-irregular-whitespace': 'off', - 'markdown/no-missing-label-refs': 'off', - } + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['docs/**/*.md', 'README.md'], + plugins: { + markdown, + }, + extends: ['markdown/recommended'], + rules: { + 'no-irregular-whitespace': 'off', + 'markdown/no-missing-label-refs': 'off', }, - [ - globalIgnores([ - 'docs/.vitepress/**', - 'bin/**', - 'dist/**', - 'build/**', - '.arkormx/**', - '.arkstack/**', - 'node_modules/**', - ]) - ], - { - rules: { - 'brace-style': [ - 'error', - '1tbs', - { 'allowSingleLine': false }, - ], - 'no-console': 'off', - 'no-thenable': 'off', - // 'no-ternary': 'error', - 'newline-before-return': 'error', - 'semi': ['error', 'never'], - 'quotes': ['error', 'single'], - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', { - 'argsIgnorePattern': '^_|_', - 'vars': 'all', - 'args': 'after-used', - 'ignoreRestSiblings': false, - 'varsIgnorePattern': '^I[A-Z]|^_', - } - ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/triple-slash-reference': ['error', { - 'path': 'always' - }] - } + }, + [ + globalIgnores([ + 'docs/.vitepress/**', + 'bin/**', + 'dist/**', + 'build/**', + '.arkormx/**', + '.arkstack/**', + 'node_modules/**', + ]), + ], + { + rules: { + 'brace-style': ['error', '1tbs', { allowSingleLine: false }], + 'no-console': 'off', + 'no-thenable': 'off', + // 'no-ternary': 'error', + 'newline-before-return': 'error', + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_|_', + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + varsIgnorePattern: '^I[A-Z]|^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + }, + ], }, -) \ No newline at end of file + }, + eslintConfigPrettier, +) diff --git a/templates/h3/public/index.json b/templates/h3/public/index.json index 3c1e30e8..9d163840 100644 --- a/templates/h3/public/index.json +++ b/templates/h3/public/index.json @@ -18,4 +18,4 @@ "p": "See the world through our own eyes." } } -} \ No newline at end of file +} diff --git a/templates/h3/src/app/http/controllers/BaseController.ts b/templates/h3/src/app/http/controllers/BaseController.ts index 43e864dd..7588ba10 100644 --- a/templates/h3/src/app/http/controllers/BaseController.ts +++ b/templates/h3/src/app/http/controllers/BaseController.ts @@ -4,18 +4,18 @@ import { Controller } from 'clear-router' import { HttpContext } from 'clear-router/types/express' export class BaseController extends Controller { - async validate, R extends RulesForData> ( - rules: R, - camelizeKeys = false - ) { + async validate, R extends RulesForData>(rules: R, camelizeKeys = false) { const data = await Validator.make(this.body, rules).validate() if (camelizeKeys) { - return Object.keys(data).reduce((result, key) => { - result[str(key).camel().toString()] = data[key as never] + return Object.keys(data).reduce( + (result, key) => { + result[str(key).camel().toString()] = data[key as never] - return result - }, {} as Record) as { [K in keyof R]: any } + return result + }, + {} as Record, + ) as { [K in keyof R]: any } } return data as { [K in keyof R]: any } diff --git a/templates/h3/src/app/http/controllers/UserController.ts b/templates/h3/src/app/http/controllers/UserController.ts index d5d2ff36..c48a6fd5 100644 --- a/templates/h3/src/app/http/controllers/UserController.ts +++ b/templates/h3/src/app/http/controllers/UserController.ts @@ -13,7 +13,7 @@ export default class UserController extends BaseController { * * @param req */ - async index () { + async index() { const users = await User.query().paginate(5) return new ResourceCollection(users) @@ -32,7 +32,7 @@ export default class UserController extends BaseController { * @param res */ @Bind() - async show (user: User) { + async show(user: User) { return new Resource(user) .additional({ status: 'success', @@ -48,7 +48,7 @@ export default class UserController extends BaseController { * * @param res */ - async create () { + async create() { return new Resource({ data: {} }) .additional({ status: 'success', @@ -64,7 +64,7 @@ export default class UserController extends BaseController { * * @param res */ - async update () { + async update() { const data = await this.validate({ name: 'string|required', age: 'numeric|required|min:30', @@ -85,7 +85,7 @@ export default class UserController extends BaseController { * * @param res */ - async destroy () { + async destroy() { return new Resource({ data: {} }) .additional({ status: 'success', diff --git a/templates/h3/src/app/http/resources/UserCollection.ts b/templates/h3/src/app/http/resources/UserCollection.ts index 4a11e604..5a10b422 100644 --- a/templates/h3/src/app/http/resources/UserCollection.ts +++ b/templates/h3/src/app/http/resources/UserCollection.ts @@ -11,7 +11,7 @@ export class UserCollection extends ResourceCollection { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/h3/src/app/http/resources/UserResource.ts b/templates/h3/src/app/http/resources/UserResource.ts index f4de6048..e5247c06 100644 --- a/templates/h3/src/app/http/resources/UserResource.ts +++ b/templates/h3/src/app/http/resources/UserResource.ts @@ -8,7 +8,7 @@ export class UserResource extends Resource { * Build the response object * @returns this */ - data () { + data() { return this.toObject() } } diff --git a/templates/h3/src/app/models/PersonalAccessToken.ts b/templates/h3/src/app/models/PersonalAccessToken.ts index 89396bac..48f9e60e 100644 --- a/templates/h3/src/app/models/PersonalAccessToken.ts +++ b/templates/h3/src/app/models/PersonalAccessToken.ts @@ -2,25 +2,25 @@ import { PersonalAccessToken as BasePersonalAccessToken } from '@arkstack/auth' import { User } from './User' export class PersonalAccessToken extends BasePersonalAccessToken { - declare name: string - declare token: string - declare abilities: string[] - declare deviceInfo: Record - declare lastUsedAt: Date - declare expiresAt: Date - declare createdAt: Date - declare updatedAt: Date + declare name: string + declare token: string + declare abilities: string[] + declare deviceInfo: Record + declare lastUsedAt: Date + declare expiresAt: Date + declare createdAt: Date + declare updatedAt: Date - protected static columns = { - userId: 'user_id', - deviceInfo: 'device_info', - lastUsedAt: 'last_used_at', - expiresAt: 'expires_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + deviceInfo: 'device_info', + lastUsedAt: 'last_used_at', + expiresAt: 'expires_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId') - } + user() { + return this.belongsTo(User, 'userId') + } } diff --git a/templates/h3/src/app/models/User.ts b/templates/h3/src/app/models/User.ts index bb4e58d5..bf3b144c 100644 --- a/templates/h3/src/app/models/User.ts +++ b/templates/h3/src/app/models/User.ts @@ -5,32 +5,30 @@ import { UserTwoFactor } from './UserTwoFactor' import { UserFactory } from 'src/database/factories/UserFactory' export class User extends BaseUser { - declare email: string - declare password: string - declare name: string - declare createdAt: Date - declare updatedAt: Date + declare email: string + declare password: string + declare name: string + declare createdAt: Date + declare updatedAt: Date - protected static override factoryClass = UserFactory + protected static override factoryClass = UserFactory - protected static override columns = { - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static override columns = { + createdAt: 'created_at', + updatedAt: 'updated_at', + } - protected override hidden = [ - 'password' - ] + protected override hidden = ['password'] - personalAccessTokens () { - return this.hasMany(PersonalAccessToken, 'userId') - } + personalAccessTokens() { + return this.hasMany(PersonalAccessToken, 'userId') + } - twoFactor () { - return this.hasOne(UserTwoFactor, 'userId') - } + twoFactor() { + return this.hasOne(UserTwoFactor, 'userId') + } - notifications () { - return this.hasMany(UserNotification, 'userId') - } + notifications() { + return this.hasMany(UserNotification, 'userId') + } } diff --git a/templates/h3/src/app/models/UserNotification.ts b/templates/h3/src/app/models/UserNotification.ts index bc04dd71..a9481b41 100644 --- a/templates/h3/src/app/models/UserNotification.ts +++ b/templates/h3/src/app/models/UserNotification.ts @@ -2,16 +2,16 @@ import { UserNotification as BaseUserNotification } from '@arkstack/notification import { User } from './User' export class UserNotification extends BaseUserNotification { - protected static columns = { - userId: 'user_id', - actionText: 'action_text', - actionLink: 'action_link', - readAt: 'read_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + actionText: 'action_text', + actionLink: 'action_link', + readAt: 'read_at', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } + user() { + return this.belongsTo(User, 'userId', 'id') + } } diff --git a/templates/h3/src/app/models/UserTwoFactor.ts b/templates/h3/src/app/models/UserTwoFactor.ts index 8d85a059..8186c98a 100644 --- a/templates/h3/src/app/models/UserTwoFactor.ts +++ b/templates/h3/src/app/models/UserTwoFactor.ts @@ -2,19 +2,19 @@ import { UserTwoFactor as BaseUserTwoFactor } from '@arkstack/auth' import { User } from './User' export class UserTwoFactor extends BaseUserTwoFactor { - protected static columns = { - userId: 'user_id', - secretCiphertext: 'secret_ciphertext', - smsCodeHash: 'sms_code_hash', - smsCodeExpiresAt: 'sms_code_expires_at', - smsCodePurpose: 'sms_code_purpose', - enabledAt: 'enabled_at', - recoveryCodeHashes: 'recovery_code_hashes', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static columns = { + userId: 'user_id', + secretCiphertext: 'secret_ciphertext', + smsCodeHash: 'sms_code_hash', + smsCodeExpiresAt: 'sms_code_expires_at', + smsCodePurpose: 'sms_code_purpose', + enabledAt: 'enabled_at', + recoveryCodeHashes: 'recovery_code_hashes', + createdAt: 'created_at', + updatedAt: 'updated_at', + } - user () { - return this.belongsTo(User, 'userId', 'id') - } -} \ No newline at end of file + user() { + return this.belongsTo(User, 'userId', 'id') + } +} diff --git a/templates/h3/src/config/app.ts b/templates/h3/src/config/app.ts index 6711600a..f8cf46f0 100644 --- a/templates/h3/src/config/app.ts +++ b/templates/h3/src/config/app.ts @@ -1,17 +1,17 @@ import { AppConfig } from '@arkstack/common' export default (): AppConfig => { - return { - env: env('APP_ENV', 'local'), - key: env('APP_KEY', 'change-me'), - url: env('APP_URL', 'http://localhost'), - host: env('APP_HOST', '0.0.0.0'), - name: env('APP_NAME', 'Arkstack'), - frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), - debug: env('APP_DEBUG', false), - timezone: env('APP_TIMEZONE', 'UTC'), - locale: env('APP_LOCALE', 'en'), - fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), - faker_locale: env('APP_FAKER_LOCALE', 'en_US'), - } + return { + env: env('APP_ENV', 'local'), + key: env('APP_KEY', 'change-me'), + url: env('APP_URL', 'http://localhost'), + host: env('APP_HOST', '0.0.0.0'), + name: env('APP_NAME', 'Arkstack'), + frontend_url: env('FRONTEND_URL', 'http://localhost:3000'), + debug: env('APP_DEBUG', false), + timezone: env('APP_TIMEZONE', 'UTC'), + locale: env('APP_LOCALE', 'en'), + fallback_locale: env('APP_FALLBACK_LOCALE', 'en'), + faker_locale: env('APP_FAKER_LOCALE', 'en_US'), + } } diff --git a/templates/h3/src/config/cache.ts b/templates/h3/src/config/cache.ts index 4e632b05..0787ab2f 100644 --- a/templates/h3/src/config/cache.ts +++ b/templates/h3/src/config/cache.ts @@ -3,48 +3,48 @@ import type { CacheConfig } from '@arkstack/cache' import path from 'node:path' export default (): CacheConfig => { - return { - /** - * Default Cache Store - * - * The store used by the framework whenever a store is not explicitly - * requested. Supported drivers: memory, file, redis, database. - */ - default: env('CACHE_STORE', 'memory'), + return { + /** + * Default Cache Store + * + * The store used by the framework whenever a store is not explicitly + * requested. Supported drivers: memory, file, redis, database. + */ + default: env('CACHE_STORE', 'memory'), - /** - * Cache Key Prefix - * - * Prepended to every cache key to avoid collisions between applications - * that share the same backing store. - */ - prefix: env('CACHE_PREFIX', 'arkstack_cache_'), + /** + * Cache Key Prefix + * + * Prepended to every cache key to avoid collisions between applications + * that share the same backing store. + */ + prefix: env('CACHE_PREFIX', 'arkstack_cache_'), - /** - * Cache Stores - * - * Configure as many stores as you like. Each is referenced by name via - * `Cache.store('')`. - */ - stores: { - memory: { - driver: 'memory', - }, - file: { - driver: 'file', - path: path.join(Arkstack.rootDir(), './storage/framework/cache'), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_CACHE_DB', 1), - }, - database: { - driver: 'database', - table: env('CACHE_TABLE', 'cache'), - }, - }, - } + /** + * Cache Stores + * + * Configure as many stores as you like. Each is referenced by name via + * `Cache.store('')`. + */ + stores: { + memory: { + driver: 'memory', + }, + file: { + driver: 'file', + path: path.join(Arkstack.rootDir(), './storage/framework/cache'), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_CACHE_DB', 1), + }, + database: { + driver: 'database', + table: env('CACHE_TABLE', 'cache'), + }, + }, + } } diff --git a/templates/h3/src/config/cors.ts b/templates/h3/src/config/cors.ts index 881e57b5..f779185f 100644 --- a/templates/h3/src/config/cors.ts +++ b/templates/h3/src/config/cors.ts @@ -1,12 +1,9 @@ import appConfig from './app' export default () => { - const app = appConfig() + const app = appConfig() - return { - allowed_origins: [ - app.url, - app.frontend_url, - ].filter(Boolean) - } -} \ No newline at end of file + return { + allowed_origins: [app.url, app.frontend_url].filter(Boolean), + } +} diff --git a/templates/h3/src/config/database.ts b/templates/h3/src/config/database.ts index a316bbd4..46c7f535 100644 --- a/templates/h3/src/config/database.ts +++ b/templates/h3/src/config/database.ts @@ -1,31 +1,31 @@ import { DatabaseConfig } from '@arkstack/database' export default (): DatabaseConfig => { - return { - /** - * Default Database Connection - * - * The connection used whenever one is not explicitly requested. - * Supported drivers: postgres. - */ - default: env('DB_CONNECTION', 'pgsql'), + return { + /** + * Default Database Connection + * + * The connection used whenever one is not explicitly requested. + * Supported drivers: postgres. + */ + default: env('DB_CONNECTION', 'pgsql'), - /** - * Database Connections - * - * Credentials are provided as discrete fields. When `url` (or - * `DATABASE_URL`) is set it takes precedence over the discrete fields. - */ - connections: { - pgsql: { - driver: 'postgres', - url: env('DATABASE_URL'), - host: env('DB_HOST', '127.0.0.1'), - port: env('DB_PORT', 5432), - user: env('DB_USERNAME', 'postgres'), - database: env('DB_DATABASE', 'arkstack'), - password: env('DB_PASSWORD', 'postgres'), - }, - }, - } + /** + * Database Connections + * + * Credentials are provided as discrete fields. When `url` (or + * `DATABASE_URL`) is set it takes precedence over the discrete fields. + */ + connections: { + pgsql: { + driver: 'postgres', + url: env('DATABASE_URL'), + host: env('DB_HOST', '127.0.0.1'), + port: env('DB_PORT', 5432), + user: env('DB_USERNAME', 'postgres'), + database: env('DB_DATABASE', 'arkstack'), + password: env('DB_PASSWORD', 'postgres'), + }, + }, + } } diff --git a/templates/h3/src/config/filesystem.ts b/templates/h3/src/config/filesystem.ts index 849f2f34..ac637d79 100644 --- a/templates/h3/src/config/filesystem.ts +++ b/templates/h3/src/config/filesystem.ts @@ -3,84 +3,84 @@ import { FilesystemConfig } from '@arkstack/filesystem' import path from 'node:path' export default (): FilesystemConfig => { - return { - /** - * Default filesystem disk to be used by the framework. - */ - default: env('FILESYSTEM_DISK', 'local'), + return { + /** + * Default filesystem disk to be used by the framework. + */ + default: env('FILESYSTEM_DISK', 'local'), - /** - * Available disks and their configurations. - * - * You can configure as many disks as you want, and even have multiple disks - * using the same driver. - * - * Supported drivers: local, ftp, s3 - */ - disks: { - local: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app'), - visibility: 'public', - }, - public: { - driver: 'local', - root: path.join(Arkstack.rootDir(), './storage/app/public'), - visibility: 'public' - }, - ftp: { - driver: 'ftp', - host: env('FTP_HOST'), - username: env('FTP_USERNAME'), - password: env('FTP_PASSWORD'), - port: env('FTP_PORT', 21), - verbose: env('FTP_VERBOSE', false), - privateKey: env('FTP_PRIVATE_KEY'), - }, - s3: { - driver: 's3', - key: env('AWS_ACCESS_KEY_ID'), - secret: env('AWS_SECRET_ACCESS_KEY'), - region: env('AWS_DEFAULT_REGION'), - bucket: env('AWS_BUCKET'), - url: env('AWS_URL'), - endpoint: env('AWS_ENDPOINT'), - visibility: 'public', - }, - gcs: { - driver: 'gcs', - projectId: env('GOOGLE_CLOUD_PROJECT'), - keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), - bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), - visibility: 'private', - usingUniformAcl: true, - } - }, + /** + * Available disks and their configurations. + * + * You can configure as many disks as you want, and even have multiple disks + * using the same driver. + * + * Supported drivers: local, ftp, s3 + */ + disks: { + local: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app'), + visibility: 'public', + }, + public: { + driver: 'local', + root: path.join(Arkstack.rootDir(), './storage/app/public'), + visibility: 'public', + }, + ftp: { + driver: 'ftp', + host: env('FTP_HOST'), + username: env('FTP_USERNAME'), + password: env('FTP_PASSWORD'), + port: env('FTP_PORT', 21), + verbose: env('FTP_VERBOSE', false), + privateKey: env('FTP_PRIVATE_KEY'), + }, + s3: { + driver: 's3', + key: env('AWS_ACCESS_KEY_ID'), + secret: env('AWS_SECRET_ACCESS_KEY'), + region: env('AWS_DEFAULT_REGION'), + bucket: env('AWS_BUCKET'), + url: env('AWS_URL'), + endpoint: env('AWS_ENDPOINT'), + visibility: 'public', + }, + gcs: { + driver: 'gcs', + projectId: env('GOOGLE_CLOUD_PROJECT'), + keyFilename: env('GOOGLE_APPLICATION_CREDENTIALS'), + bucket: env('GOOGLE_CLOUD_STORAGE_BUCKET'), + visibility: 'private', + usingUniformAcl: true, + }, + }, - /** - * Optional symbolic links to create when the `storage:link` command is executed. - * The key of the object represents the link location, while the value represents the target location. - */ - links: { - [path.join(Arkstack.rootDir(), './public/storage')]: path.join(Arkstack.rootDir(), './storage/app/public'), - }, + /** + * Optional symbolic links to create when the `storage:link` command is executed. + * The key of the object represents the link location, while the value represents the target location. + */ + links: { + [path.join(Arkstack.rootDir(), './public/storage')]: path.join(Arkstack.rootDir(), './storage/app/public'), + }, - /** - * Optional file name generator function. - * If provided, this function will be used to generate unique file names - * for uploaded files when using the `saveFile` method of the Storage class. - * - * @param originalName The original name of the file - * @returns A unique file name - * - * fileNameGenerator: (originalName: string) => { - * const timestamp = Date.now() - * const randomString = Math.random().toString(36).substring(2, 8) - * const extension = path.extname(originalName) - * const baseName = path.basename(originalName, extension) - * - * return `${baseName}-${timestamp}-${randomString}${extension}` - * }, - */ - } + /** + * Optional file name generator function. + * If provided, this function will be used to generate unique file names + * for uploaded files when using the `saveFile` method of the Storage class. + * + * @param originalName The original name of the file + * @returns A unique file name + * + * fileNameGenerator: (originalName: string) => { + * const timestamp = Date.now() + * const randomString = Math.random().toString(36).substring(2, 8) + * const extension = path.extname(originalName) + * const baseName = path.basename(originalName, extension) + * + * return `${baseName}-${timestamp}-${randomString}${extension}` + * }, + */ + } } diff --git a/templates/h3/src/config/middleware.ts b/templates/h3/src/config/middleware.ts index 31bd6db3..0427aadd 100644 --- a/templates/h3/src/config/middleware.ts +++ b/templates/h3/src/config/middleware.ts @@ -19,10 +19,8 @@ export default (): MiddlewareConfig => { function ({ req }, next) { useH3UploadContext(req) next() - } - ], - after: [ - requestLogger() + }, ], + after: [requestLogger()], } -} +} diff --git a/templates/h3/src/config/notifications.ts b/templates/h3/src/config/notifications.ts index 14d8a10b..7a745246 100644 --- a/templates/h3/src/config/notifications.ts +++ b/templates/h3/src/config/notifications.ts @@ -3,157 +3,157 @@ import { NotificationConfig } from '@arkstack/notifications' import { join } from 'node:path' export default (): NotificationConfig => { - return { - /** - * Default Notification Driver - * - * The default notifications driver that is utilized for outgoing messages. - */ - default_driver: env('NOTIFICATION_DRIVER', 'mail'), + return { + /** + * Default Notification Driver + * + * The default notifications driver that is utilized for outgoing messages. + */ + default_driver: env('NOTIFICATION_DRIVER', 'mail'), + /** + * Notification Drivers + * + * Notifications will be sent using one of the configured drivers. + */ + drivers: { + mail: { + transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', + from: { + name: env('MAIL_FROM_NAME', 'Arcstack'), + address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), + }, + test_address: env('MAIL_TEST_ADDRESS'), + }, + sms: { + transport: 'africastalking', + from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), + }, + db: { + table: 'user_notifications', + }, + realtime: { /** - * Notification Drivers - * - * Notifications will be sent using one of the configured drivers. + * pusher | firebase | firebase-database | polling | memory */ - drivers: { - mail: { - transport: env('MAIL_TRANSPORT', 'smtp') as 'smtp' | 'file', - from: { - name: env('MAIL_FROM_NAME', 'Arcstack'), - address: env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), - }, - test_address: env('MAIL_TEST_ADDRESS'), - }, - sms: { - transport: 'africastalking', - from: env('SMS_FROM', env('AFRICASTALKING_SENDER_ID', env('TWILIO_FROM'))), - }, - db: { - table: 'user_notifications', - }, - realtime: { - /** - * pusher | firebase | firebase-database | polling | memory - */ - transport: env('REALTIME_TRANSPORT', 'pusher'), - channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), - event: env('REALTIME_EVENT', 'notification'), - store: env('REALTIME_STORE', false), - }, + transport: env('REALTIME_TRANSPORT', 'pusher'), + channel_prefix: env('REALTIME_CHANNEL_PREFIX', 'user.'), + event: env('REALTIME_EVENT', 'notification'), + store: env('REALTIME_STORE', false), + }, + }, + transports: { + /** + * SMTP Notifications Transport + * + * Outgoing notifications will be sent as mail using SMTP. + */ + smtp: { + host: env('MAIL_HOST', 'localhost'), + port: env('MAIL_PORT', 1025), + secure: env('MAIL_SECURE', false), + auth: { + user: env('MAIL_USERNAME', 'user@example.com'), + pass: env('MAIL_PASSWORD', 'password'), }, - transports: { - /** - * SMTP Notifications Transport - * - * Outgoing notifications will be sent as mail using SMTP. - */ - smtp: { - host: env('MAIL_HOST', 'localhost'), - port: env('MAIL_PORT', 1025), - secure: env('MAIL_SECURE', false), - auth: { - user: env('MAIL_USERNAME', 'user@example.com'), - pass: env('MAIL_PASSWORD', 'password'), - }, - }, + }, - /** - * File Notifications Transport - * - * Outgoing notifications will be sent as mail but stored locally in a file - * for testing purposes. - */ - file: { - /** - * Mails Directory - * - * Outgoing mails will be intercepted and stored in this directory. - */ - directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), - }, + /** + * File Notifications Transport + * + * Outgoing notifications will be sent as mail but stored locally in a file + * for testing purposes. + */ + file: { + /** + * Mails Directory + * + * Outgoing mails will be intercepted and stored in this directory. + */ + directory: env('MAIL_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/mails')), + }, - /** - * Africa's Talking Notifications Transport - * - * Outgoing notifications will be sent as SMS via Africa's Talking. - */ - africastalking: { - username: env('AFRICASTALKING_USERNAME', 'sandbox'), - apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), - senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), - }, + /** + * Africa's Talking Notifications Transport + * + * Outgoing notifications will be sent as SMS via Africa's Talking. + */ + africastalking: { + username: env('AFRICASTALKING_USERNAME', 'sandbox'), + apiKey: env('AFRICASTALKING_API_KEY', 'sandbox'), + senderId: env('AFRICASTALKING_SENDER_ID', env('SMS_FROM', 'Arkstack')), + }, - /** - * Twilio Notifications Transport - * - * Outgoing notifications will be sent as SMS via Twilio. - */ - twilio: { - accountSid: env('TWILIO_ACCOUNT_SID'), - authToken: env('TWILIO_AUTH_TOKEN'), - from: env('TWILIO_FROM', env('SMS_FROM')), - }, + /** + * Twilio Notifications Transport + * + * Outgoing notifications will be sent as SMS via Twilio. + */ + twilio: { + accountSid: env('TWILIO_ACCOUNT_SID'), + authToken: env('TWILIO_AUTH_TOKEN'), + from: env('TWILIO_FROM', env('SMS_FROM')), + }, - /** - * Pusher Realtime Transport - * - * Realtime notifications will be broadcast over Pusher Channels. - */ - pusher: { - app_id: env('PUSHER_APP_ID', ''), - key: env('PUSHER_KEY', ''), - secret: env('PUSHER_SECRET', ''), - cluster: env('PUSHER_CLUSTER', 'mt1'), - use_tls: env('PUSHER_USE_TLS', true), - }, + /** + * Pusher Realtime Transport + * + * Realtime notifications will be broadcast over Pusher Channels. + */ + pusher: { + app_id: env('PUSHER_APP_ID', ''), + key: env('PUSHER_KEY', ''), + secret: env('PUSHER_SECRET', ''), + cluster: env('PUSHER_CLUSTER', 'mt1'), + use_tls: env('PUSHER_USE_TLS', true), + }, - /** - * Firebase Realtime Transport - * - * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. - */ - firebase: { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - web: { - api_key: env('FIREBASE_API_KEY', ''), - app_id: env('FIREBASE_APP_ID', ''), - messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), - vapid_key: env('FIREBASE_VAPID_KEY', ''), - }, - }, + /** + * Firebase Realtime Transport + * + * Realtime notifications will be broadcast over Firebase Cloud Messaging topics. + */ + firebase: { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + web: { + api_key: env('FIREBASE_API_KEY', ''), + app_id: env('FIREBASE_APP_ID', ''), + messaging_sender_id: env('FIREBASE_MESSAGING_SENDER_ID', ''), + vapid_key: env('FIREBASE_VAPID_KEY', ''), + }, + }, - /** - * Firebase Realtime Database Transport - * - * Realtime notifications will be broadcast over Realtime Database channels. - */ - 'firebase-database': { - project_id: env('FIREBASE_PROJECT_ID', ''), - client_email: env('FIREBASE_CLIENT_EMAIL', ''), - private_key: env('FIREBASE_PRIVATE_KEY', ''), - app_name: env('APP_NAME', 'Arkstack'), - admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), - database_url: env('FIREBASE_DATABASE_URL', ''), - web: { - api_key: env('FIREBASE_API_KEY', ''), - auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), - app_id: env('FIREBASE_APP_ID', ''), - }, - }, + /** + * Firebase Realtime Database Transport + * + * Realtime notifications will be broadcast over Realtime Database channels. + */ + 'firebase-database': { + project_id: env('FIREBASE_PROJECT_ID', ''), + client_email: env('FIREBASE_CLIENT_EMAIL', ''), + private_key: env('FIREBASE_PRIVATE_KEY', ''), + app_name: env('APP_NAME', 'Arkstack'), + admin_sdk_path: env('FIREBASE_ADMINSDK', 'firebase-adminsdk.json'), + database_url: env('FIREBASE_DATABASE_URL', ''), + web: { + api_key: env('FIREBASE_API_KEY', ''), + auth_domain: env('FIREBASE_AUTH_DOMAIN', ''), + app_id: env('FIREBASE_APP_ID', ''), + }, + }, - /** - * Polling Realtime Transport - * - * Nothing is pushed; clients poll on this interval (ms) instead. - */ - polling: { - interval: env('REALTIME_POLL_INTERVAL', 5000), - }, - } - } + /** + * Polling Realtime Transport + * + * Nothing is pushed; clients poll on this interval (ms) instead. + */ + polling: { + interval: env('REALTIME_POLL_INTERVAL', 5000), + }, + }, + } } diff --git a/templates/h3/src/config/queue.ts b/templates/h3/src/config/queue.ts index f1bd0566..68cf03fe 100644 --- a/templates/h3/src/config/queue.ts +++ b/templates/h3/src/config/queue.ts @@ -1,43 +1,43 @@ import type { QueueConfig } from '@arkstack/queue' export default (): QueueConfig => { - return { - /** - * Default Queue Connection - * - * The connection used when a job is dispatched without specifying one. - * The `sync` connection runs jobs immediately (no worker required) and is - * a sensible default for local development and tests. - * - * Supported drivers: sync, database, redis. - */ - default: env('QUEUE_CONNECTION', 'sync'), + return { + /** + * Default Queue Connection + * + * The connection used when a job is dispatched without specifying one. + * The `sync` connection runs jobs immediately (no worker required) and is + * a sensible default for local development and tests. + * + * Supported drivers: sync, database, redis. + */ + default: env('QUEUE_CONNECTION', 'sync'), - /** - * Queue Connections - * - * Configure a connection per backend. Run a worker with `ark queue:work` - * to process jobs on the database and redis connections. - */ - connections: { - sync: { - driver: 'sync', - }, - database: { - driver: 'database', - table: env('QUEUE_TABLE', 'jobs'), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - redis: { - driver: 'redis', - host: env('REDIS_HOST', '127.0.0.1'), - port: env('REDIS_PORT', 6379), - password: env('REDIS_PASSWORD'), - db: env('REDIS_QUEUE_DB', 0), - queue: env('QUEUE_NAME', 'default'), - retryAfter: env('QUEUE_RETRY_AFTER', 90), - }, - }, - } + /** + * Queue Connections + * + * Configure a connection per backend. Run a worker with `ark queue:work` + * to process jobs on the database and redis connections. + */ + connections: { + sync: { + driver: 'sync', + }, + database: { + driver: 'database', + table: env('QUEUE_TABLE', 'jobs'), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + redis: { + driver: 'redis', + host: env('REDIS_HOST', '127.0.0.1'), + port: env('REDIS_PORT', 6379), + password: env('REDIS_PASSWORD'), + db: env('REDIS_QUEUE_DB', 0), + queue: env('QUEUE_NAME', 'default'), + retryAfter: env('QUEUE_RETRY_AFTER', 90), + }, + }, + } } diff --git a/templates/h3/src/config/resources.ts b/templates/h3/src/config/resources.ts index 0561b2b9..3ab17a98 100644 --- a/templates/h3/src/config/resources.ts +++ b/templates/h3/src/config/resources.ts @@ -9,44 +9,44 @@ import type { ResoraConfig } from 'resora' * resora accepts at runtime but doesn't surface in its narrower config type. */ export default (): ResoraConfig => { - return { - preferredCase: 'camel', - responseStructure: { - wrap: true, - rootKey: 'data', - }, - paginatedExtras: ['meta', 'links'], - baseUrl: env('APP_URL', 'http://localhost'), - pageName: 'page', - paginatedLinks: { - first: 'first', - last: 'last', - prev: 'prev', - next: 'next', - }, - paginatedMeta: { - to: 'to', - from: 'from', - links: 'links', - path: 'path', - total: 'total', - per_page: 'per_page', - last_page: 'last_page', - current_page: 'current_page', - }, - cursorMeta: { - previous: 'previous', - next: 'next', - }, - resourcesDir: 'src/app/http/resources', - localStubsDir: 'node_modules/@arkstack/driver-express/stubs', - stubs: { - resource: 'resource.stub', - collection: 'resource.collection.stub', - controller: 'controller.stub', - api: 'controller.api.stub', - model: 'controller.model.stub', - apiResource: 'controller.api.resource.stub', - }, - } as ResoraConfig + return { + preferredCase: 'camel', + responseStructure: { + wrap: true, + rootKey: 'data', + }, + paginatedExtras: ['meta', 'links'], + baseUrl: env('APP_URL', 'http://localhost'), + pageName: 'page', + paginatedLinks: { + first: 'first', + last: 'last', + prev: 'prev', + next: 'next', + }, + paginatedMeta: { + to: 'to', + from: 'from', + links: 'links', + path: 'path', + total: 'total', + per_page: 'per_page', + last_page: 'last_page', + current_page: 'current_page', + }, + cursorMeta: { + previous: 'previous', + next: 'next', + }, + resourcesDir: 'src/app/http/resources', + localStubsDir: 'node_modules/@arkstack/driver-express/stubs', + stubs: { + resource: 'resource.stub', + collection: 'resource.collection.stub', + controller: 'controller.stub', + api: 'controller.api.stub', + model: 'controller.model.stub', + apiResource: 'controller.api.resource.stub', + }, + } as ResoraConfig } diff --git a/templates/h3/src/config/session.ts b/templates/h3/src/config/session.ts index 0a829e18..1f5fa62d 100644 --- a/templates/h3/src/config/session.ts +++ b/templates/h3/src/config/session.ts @@ -4,83 +4,80 @@ import { Str } from '@h3ravel/support' import { join } from 'node:path' export default (): SessionConfig => { - return { - secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), + return { + secret: env('SESSION_SECRET', env('APP_KEY', 'change-me')), - /** - * Session Driver - * - * The default session driver that is utilized for incoming requests. - * - * Supported: "file", "cookie", "database", - * (WIP: "apc", "memcached", "redis", "dynamodb", "array") - */ - driver: env('SESSION_DRIVER', 'file'), + /** + * Session Driver + * + * The default session driver that is utilized for incoming requests. + * + * Supported: "file", "cookie", "database", + * (WIP: "apc", "memcached", "redis", "dynamodb", "array") + */ + driver: env('SESSION_DRIVER', 'file'), - /** - * Cookie Name - * - * The name of the session cookie that is created by Arkstack. - */ - cookie: env( - 'SESSION_COOKIE', - Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session' - ), + /** + * Cookie Name + * + * The name of the session cookie that is created by Arkstack. + */ + cookie: env('SESSION_COOKIE', Str.slug(env('APP_NAME', 'arkstack'), '_') + '_session'), - /** - * Session Time To Live - * - * The number of minutes that the session is allowed to remain idle before - * it expires. - */ - ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), + /** + * Session Time To Live + * + * The number of minutes that the session is allowed to remain idle before + * it expires. + */ + ttl: env('SESSION_LIFETIME', 60 * 60 * 24 * 7), - /** - * HTTP Only Access - * - * Prevent JavaScript from accessing the value of the cookie making it - * only accessible through the HTTP protocol. - */ - http_only: true, + /** + * HTTP Only Access + * + * Prevent JavaScript from accessing the value of the cookie making it + * only accessible through the HTTP protocol. + */ + http_only: true, - /** - * HTTPS Only Access - * - * This option ensures that cookies will only be sent back to the server - * if the browser has a HTTPS connection if set to "true". - */ - secure: env('NODE_ENV', 'development') === 'production', + /** + * HTTPS Only Access + * + * This option ensures that cookies will only be sent back to the server + * if the browser has a HTTPS connection if set to "true". + */ + secure: env('NODE_ENV', 'development') === 'production', - /** - * Same-Site Cookies - * - * Determines how cookies behave when cross-site requests take - * place, and can be used to mitigate CSRF attacks. - * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - * - * Supported: "Lax", "Strict", "None", undefined - */ - same_site: 'Lax', + /** + * Same-Site Cookies + * + * Determines how cookies behave when cross-site requests take + * place, and can be used to mitigate CSRF attacks. + * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + * + * Supported: "Lax", "Strict", "None", undefined + */ + same_site: 'Lax', - /** - * Session Cookie Path - * - * Determines the path for which the cookie will be available. - */ - path: '/', - /** - * Datbase Table - * - * For the "database" session driver, this will determine the table to - * be used to store sessions. - */ - table: env('SESSION_TABLE', 'sessions'), + /** + * Session Cookie Path + * + * Determines the path for which the cookie will be available. + */ + path: '/', + /** + * Datbase Table + * + * For the "database" session driver, this will determine the table to + * be used to store sessions. + */ + table: env('SESSION_TABLE', 'sessions'), - /** - * Session Directory - * - * For the "file" session driver, the session files are placed in this directory. - */ - directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), - } + /** + * Session Directory + * + * For the "file" session driver, the session files are placed in this directory. + */ + directory: env('SESSION_FILE_PATH', join(Arkstack.rootDir(), './storage/framework/sessions')), + } } diff --git a/templates/h3/src/core/app.ts b/templates/h3/src/core/app.ts index 12bce8f2..21329ab4 100644 --- a/templates/h3/src/core/app.ts +++ b/templates/h3/src/core/app.ts @@ -7,10 +7,10 @@ import { Router } from '@arkstack/driver-h3' export default class Application extends Arkstack { /** - * Creates an instance of the Application class, initializing + * Creates an instance of the Application class, initializing * the H3 driver with the provided options and creating an H3 application instance. - * - * @param app + * + * @param app */ constructor(app?: H3) { super() @@ -28,8 +28,8 @@ export default class Application extends Arkstack { /** * Gets the ArkstackRouterContract implementation for the H3 framework. - * - * @returns + * + * @returns */ getRouter(): ArkstackRouterContract { return { @@ -39,9 +39,9 @@ export default class Application extends Arkstack { } /** - * Boots the application by mounting public assets, binding the + * Boots the application by mounting public assets, binding the * router, applying middleware, and starting the server. - * + * * @param port The numeric port to run the server on * @param defer Set to true to skip server startup */ diff --git a/templates/h3/src/core/bootstrap.ts b/templates/h3/src/core/bootstrap.ts index c69969a9..fbcb9d25 100644 --- a/templates/h3/src/core/bootstrap.ts +++ b/templates/h3/src/core/bootstrap.ts @@ -13,4 +13,4 @@ View.boot() Validator.use(fileValidatorPlugin) export const app = new Application() -export const h3App = app.getAppInstance() \ No newline at end of file +export const h3App = app.getAppInstance() diff --git a/templates/h3/src/database/factories/UserFactory.ts b/templates/h3/src/database/factories/UserFactory.ts index ce2528af..7076c8b8 100644 --- a/templates/h3/src/database/factories/UserFactory.ts +++ b/templates/h3/src/database/factories/UserFactory.ts @@ -4,13 +4,13 @@ import { User } from '@app/models/User' import { faker } from '@faker-js/faker' export class UserFactory extends ModelFactory { - protected model = User + protected model = User - protected definition (_sequence: number) { - return { - 'name': faker.person.fullName(), - 'email': faker.internet.email(), - 'password': Hash.make('password'), - } + protected definition(_sequence: number) { + return { + name: faker.person.fullName(), + email: faker.internet.email(), + password: Hash.make('password'), } + } } diff --git a/templates/h3/src/database/migrations/20260428164120_create_users_table.ts b/templates/h3/src/database/migrations/20260428164120_create_users_table.ts index 8c6f52ac..cb489074 100644 --- a/templates/h3/src/database/migrations/20260428164120_create_users_table.ts +++ b/templates/h3/src/database/migrations/20260428164120_create_users_table.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUsersTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('users', (table) => { - table.id('id', 'uuid').primary() - table.string('name') - table.string('email').unique().index() - table.string('password') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("users", (table) => { + table.id("id", "uuid").primary(); + table.string("name"); + table.string("email").unique().index(); + table.string("password"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('users') - } -} \ No newline at end of file + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("users"); + } +} diff --git a/templates/h3/src/database/migrations/20260428164833_createpasswordresetstable.ts b/templates/h3/src/database/migrations/20260428164833_createpasswordresetstable.ts index 9315d10b..f6dc4083 100644 --- a/templates/h3/src/database/migrations/20260428164833_createpasswordresetstable.ts +++ b/templates/h3/src/database/migrations/20260428164833_createpasswordresetstable.ts @@ -1,17 +1,17 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePasswordResetsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('password_resets', (table) => { - table.id('id', 'uuid').primary() - table.string('email').nullable().index() - table.string('phone').nullable().index() - table.string('token') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("password_resets", (table) => { + table.id("id", "uuid").primary(); + table.string("email").nullable().index(); + table.string("phone").nullable().index(); + table.string("token"); + table.timestamps(); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('password_resets') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("password_resets"); + } } diff --git a/templates/h3/src/database/migrations/20260428165239_create_person_access_tokens_table.ts b/templates/h3/src/database/migrations/20260428165239_create_person_access_tokens_table.ts index 252d0f47..45e1249b 100644 --- a/templates/h3/src/database/migrations/20260428165239_create_person_access_tokens_table.ts +++ b/templates/h3/src/database/migrations/20260428165239_create_person_access_tokens_table.ts @@ -1,23 +1,29 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreatePersonAccessTokensTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('personal_access_tokens', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('personalAccessTokens') - table.string('name') - table.text('token').unique() - table.json('abilities').nullable() - table.json('deviceInfo').nullable().map('device_info') - table.date('lastUsedAt').nullable().map('last_used_at') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("personal_access_tokens", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("personalAccessTokens"); + table.string("name"); + table.text("token").unique(); + table.json("abilities").nullable(); + table.json("deviceInfo").nullable().map("device_info"); + table.date("lastUsedAt").nullable().map("last_used_at"); + table.date("expiresAt").nullable().map("expires_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('personal_access_tokens') - } -} + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("personal_access_tokens"); + } +} diff --git a/templates/h3/src/database/migrations/20260505110000_create_user_two_factors_table.ts b/templates/h3/src/database/migrations/20260505110000_create_user_two_factors_table.ts index 7542293b..1647dda3 100644 --- a/templates/h3/src/database/migrations/20260505110000_create_user_two_factors_table.ts +++ b/templates/h3/src/database/migrations/20260505110000_create_user_two_factors_table.ts @@ -1,24 +1,33 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserTwoFactorsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_two_factors', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('twoFactor') - table.enum('method', ['authenticator', 'sms']).nullable().enumName('TwoFactorMethod') - table.string('secretCiphertext').nullable().map('secret_ciphertext') - table.string('smsCodeHash').nullable().map('sms_code_hash') - table.date('smsCodeExpiresAt').nullable().map('sms_code_expires_at') - table.string('smsCodePurpose').nullable().map('sms_code_purpose') - table.date('enabledAt').nullable().map('enabled_at') - table.json('recoveryCodeHashes').nullable().map('recovery_code_hashes') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_two_factors", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("twoFactor"); + table + .enum("method", ["authenticator", "sms"]) + .nullable() + .enumName("TwoFactorMethod"); + table.string("secretCiphertext").nullable().map("secret_ciphertext"); + table.string("smsCodeHash").nullable().map("sms_code_hash"); + table.date("smsCodeExpiresAt").nullable().map("sms_code_expires_at"); + table.string("smsCodePurpose").nullable().map("sms_code_purpose"); + table.date("enabledAt").nullable().map("enabled_at"); + table.json("recoveryCodeHashes").nullable().map("recovery_code_hashes"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_two_factors') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_two_factors"); + } } diff --git a/templates/h3/src/database/migrations/20260505111000_create_user_notifications_table.ts b/templates/h3/src/database/migrations/20260505111000_create_user_notifications_table.ts index 7690899b..8234a972 100644 --- a/templates/h3/src/database/migrations/20260505111000_create_user_notifications_table.ts +++ b/templates/h3/src/database/migrations/20260505111000_create_user_notifications_table.ts @@ -1,24 +1,30 @@ -import { Migration, SchemaBuilder } from 'arkormx' +import { Migration, SchemaBuilder } from "arkormx"; export default class CreateUserNotificationsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('user_notifications', (table) => { - table.id('id', 'uuid').primary() - table.uuid('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('notifications') - table.string('type').nullable() - table.string('title') - table.text('description') - table.string('actionText').nullable().map('action_text') - table.string('actionLink').nullable().map('action_link') - table.json('meta').nullable() - table.date('readAt').nullable().map('read_at') - table.timestamps() - table.index(['userId']) - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("user_notifications", (table) => { + table.id("id", "uuid").primary(); + table + .uuid("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("notifications"); + table.string("type").nullable(); + table.string("title"); + table.text("description"); + table.string("actionText").nullable().map("action_text"); + table.string("actionLink").nullable().map("action_link"); + table.json("meta").nullable(); + table.date("readAt").nullable().map("read_at"); + table.timestamps(); + table.index(["userId"]); + }); + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('user_notifications') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("user_notifications"); + } } diff --git a/templates/h3/src/database/migrations/20260524000000_create_sessions_table.ts b/templates/h3/src/database/migrations/20260524000000_create_sessions_table.ts index d61c2d64..43a0e525 100644 --- a/templates/h3/src/database/migrations/20260524000000_create_sessions_table.ts +++ b/templates/h3/src/database/migrations/20260524000000_create_sessions_table.ts @@ -1,16 +1,16 @@ import { Migration, SchemaBuilder } from 'arkormx' export default class CreateSessionsTableMigration extends Migration { - public async up (schema: SchemaBuilder): Promise { - schema.createTable('sessions', (table) => { - table.string('id').primary() - table.text('payload') - table.date('expiresAt').nullable().map('expires_at') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable('sessions', (table) => { + table.string('id').primary() + table.text('payload') + table.date('expiresAt').nullable().map('expires_at') + table.timestamps() + }) + } - public async down (schema: SchemaBuilder): Promise { - schema.dropTable('sessions') - } + public async down(schema: SchemaBuilder): Promise { + schema.dropTable('sessions') + } } diff --git a/templates/h3/src/database/seeders/DatabaseSeeder.ts b/templates/h3/src/database/seeders/DatabaseSeeder.ts index 2e3d0cb6..23079b78 100644 --- a/templates/h3/src/database/seeders/DatabaseSeeder.ts +++ b/templates/h3/src/database/seeders/DatabaseSeeder.ts @@ -2,9 +2,7 @@ import { Seeder } from '@arkstack/database' import { UserSeeder } from './UserSeeder' export class DatabaseSeeder extends Seeder { - public async run (): Promise { - this.call([ - UserSeeder - ]) - } + public async run(): Promise { + this.call([UserSeeder]) + } } diff --git a/templates/h3/src/database/seeders/UserSeeder.ts b/templates/h3/src/database/seeders/UserSeeder.ts index 66b32f16..fcfb7800 100644 --- a/templates/h3/src/database/seeders/UserSeeder.ts +++ b/templates/h3/src/database/seeders/UserSeeder.ts @@ -2,7 +2,7 @@ import { Seeder } from '@arkstack/database' import { User } from 'src/app/models/User' export class UserSeeder extends Seeder { - public async run (): Promise { - User.factory(5).create() - } + public async run(): Promise { + User.factory(5).create() + } } diff --git a/templates/h3/src/routes/console.ts b/templates/h3/src/routes/console.ts index bbb354e6..c872552f 100644 --- a/templates/h3/src/routes/console.ts +++ b/templates/h3/src/routes/console.ts @@ -12,5 +12,7 @@ import { Schedule } from '@arkstack/scheduler' */ // Schedule.command('cache:prune').hourly() -Schedule.call(() => console.log('tick')).everyFiveMinutes().withoutOverlapping() +Schedule.call(() => console.log('tick')) + .everyFiveMinutes() + .withoutOverlapping() // Schedule.exec('backup.sh').dailyAt('01:30').onOneServer() diff --git a/templates/h3/src/server.ts b/templates/h3/src/server.ts index ad0d9979..d774edfb 100644 --- a/templates/h3/src/server.ts +++ b/templates/h3/src/server.ts @@ -1,3 +1,3 @@ import { app } from './core/bootstrap' -await app.startup() \ No newline at end of file +await app.startup() diff --git a/templates/h3/tests/basic.test.ts b/templates/h3/tests/basic.test.ts index 7d37304b..de12e910 100644 --- a/templates/h3/tests/basic.test.ts +++ b/templates/h3/tests/basic.test.ts @@ -15,15 +15,15 @@ const rwd = path.join(process.cwd(), '/templates/h3') if (existsSync(rwd)) Arkstack.setRootDir(rwd) describe('starter template', () => { - it('runs a basic assertion', () => { - expect(1 + 1).toBe(2) - }) + it('runs a basic assertion', () => { + expect(1 + 1).toBe(2) + }) - it('can test a route', async () => { - await request(app).get('/api').contains('{"status":"OK"}') - }) + it('can test a route', async () => { + await request(app).get('/api').contains('{"status":"OK"}') + }) - it('can test an api route', async () => { - await request(app).get('/api/users').contains('"status":"success","message":"OK"') - }) + it('can test an api route', async () => { + await request(app).get('/api/users').contains('"status":"success","message":"OK"') + }) }) diff --git a/templates/h3/tests/decorators.test.ts b/templates/h3/tests/decorators.test.ts index 08bfc0fb..38d46101 100644 --- a/templates/h3/tests/decorators.test.ts +++ b/templates/h3/tests/decorators.test.ts @@ -10,35 +10,35 @@ import { Router } from 'clear-router/h3' import { request } from 'parasito' describe('@Bind() — Controllers', () => { - let app: H3 + let app: H3 - const setupApp = async (): Promise => { - Router.apply(app) - } + const setupApp = async (): Promise => { + Router.apply(app) + } - beforeEach(() => { - Router.reset() - Container.clear() + beforeEach(() => { + Router.reset() + Container.clear() - app = new H3() - }) + app = new H3() + }) - class UserController extends BaseController { - @Bind() - async test (req: Request) { - return req.path ?? '' - } + class UserController extends BaseController { + @Bind() + async test(req: Request) { + return req.path ?? '' } - - it('can setup basic route', async () => { - Router.get('/test', () => 'OK') - await setupApp() - await request(app).get('/test').expect(200).expect('OK') - }) - - it('can use container injection in controllers', async () => { - Router.get('/test/:id', [UserController, 'test']) - await setupApp() - await request(app).get('/test/1541').expect(200).expect('/test/1541') - }) -}) \ No newline at end of file + } + + it('can setup basic route', async () => { + Router.get('/test', () => 'OK') + await setupApp() + await request(app).get('/test').expect(200).expect('OK') + }) + + it('can use container injection in controllers', async () => { + Router.get('/test/:id', [UserController, 'test']) + await setupApp() + await request(app).get('/test/1541').expect(200).expect('/test/1541') + }) +}) diff --git a/templates/h3/tsconfig.json b/templates/h3/tsconfig.json index 5bc93038..5722af5c 100644 --- a/templates/h3/tsconfig.json +++ b/templates/h3/tsconfig.json @@ -1,3 +1,3 @@ { "extends": "./.arkstack/tsconfig.json" -} \ No newline at end of file +} diff --git a/templates/h3/vitest.config.ts b/templates/h3/vitest.config.ts index b10989bd..59d953a2 100644 --- a/templates/h3/vitest.config.ts +++ b/templates/h3/vitest.config.ts @@ -1,21 +1,21 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ - oxc: { - decorator: { - legacy: true, - emitDecoratorMetadata: true, - } + oxc: { + decorator: { + legacy: true, + emitDecoratorMetadata: true, }, - resolve: { - tsconfigPaths: true - }, - test: { - environment: 'node', - include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - env: { - NODE_ENV: 'test', - VERBOSITY: '0' - }, + }, + resolve: { + tsconfigPaths: true, + }, + test: { + environment: 'node', + include: ['tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + env: { + NODE_ENV: 'test', + VERBOSITY: '0', }, + }, }) diff --git a/tests/database/CreateDatabaseClient.test.ts b/tests/database/CreateDatabaseClient.test.ts index 0a486821..54d79c09 100644 --- a/tests/database/CreateDatabaseClient.test.ts +++ b/tests/database/CreateDatabaseClient.test.ts @@ -6,67 +6,77 @@ import { beforeEach } from 'node:test' let inc: number = new Date().getTime() class User extends Model { - declare id: number - declare name: string - declare email: string + declare id: number + declare name: string + declare email: string } // const describer = process.env.NODE_CI ? describe.skip : describe describe('Modeling Dats', () => { - beforeEach(async () => { - inc = new Date().getTime() + beforeEach(async () => { + inc = new Date().getTime() + }) + + it('Can create a new model', async () => { + const user = await User.query().create({ + email: `john.doe${inc}@example.com`, + password: 'password123', + name: 'John Doe', }) - it('Can create a new model', async () => { - const user = await User.query().create({ - email: `john.doe${inc}@example.com`, - password: 'password123', - name: 'John Doe', - }) - - expect(user).toBeDefined() - expect(user.id).toBeDefined() - expect(user.email).toBe(`john.doe${inc}@example.com`) - expect(user.name).toBe('John Doe') - }) + expect(user).toBeDefined() + expect(user.id).toBeDefined() + expect(user.email).toBe(`john.doe${inc}@example.com`) + expect(user.name).toBe('John Doe') + }) - it('can get a model', async () => { - const user = await User.query().where({ email: `john.doe${inc}@example.com` }).first() + it('can get a model', async () => { + const user = await User.query() + .where({ email: `john.doe${inc}@example.com` }) + .first() - expect(user).toBeDefined() - expect(user?.id).toBeDefined() - expect(user?.email).toBe(`john.doe${inc}@example.com`) - expect(user?.name).toBe('John Doe') - }) + expect(user).toBeDefined() + expect(user?.id).toBeDefined() + expect(user?.email).toBe(`john.doe${inc}@example.com`) + expect(user?.name).toBe('John Doe') + }) - it('can update a model', async () => { - const user = await User.query().where({ email: `john.doe${inc}@example.com` }).first() + it('can update a model', async () => { + const user = await User.query() + .where({ email: `john.doe${inc}@example.com` }) + .first() - expect(user).toBeDefined() + expect(user).toBeDefined() - if (user) { - user.name = 'Jane Doe' - await user.save() + if (user) { + user.name = 'Jane Doe' + await user.save() - const updatedUser = await User.query().where({ email: `john.doe${inc}@example.com` }).first() + const updatedUser = await User.query() + .where({ email: `john.doe${inc}@example.com` }) + .first() - expect(updatedUser).toBeDefined() - expect(updatedUser?.name).toBe('Jane Doe') - } - }) + expect(updatedUser).toBeDefined() + expect(updatedUser?.name).toBe('Jane Doe') + } + }) - it('can delete a model', async () => { - const user = await User.query().where({ email: `john.doe${inc}@example.com` }).first() + it('can delete a model', async () => { + const user = await User.query() + .where({ email: `john.doe${inc}@example.com` }) + .first() - expect(user).toBeDefined() + expect(user).toBeDefined() - if (user) { - await user.delete() + if (user) { + await user.delete() - const deletedUser = await User.query().where({ email: `john.doe${inc}@example.com` }).first() + const deletedUser = await User.query() + .where({ email: `john.doe${inc}@example.com` }) + .first() - expect(deletedUser).toBeNull() - } - }) + expect(deletedUser).toBeNull() + } + }) }) diff --git a/tests/integration/express.test.ts b/tests/integration/express.test.ts index 2cd9ce64..6781664d 100644 --- a/tests/integration/express.test.ts +++ b/tests/integration/express.test.ts @@ -7,170 +7,152 @@ import { Router } from 'clear-router/express' import request from 'parasito' describe('Express App (JS)', () => { - let app: express.Application - let router: ExRouter - - beforeEach(() => { - Router.configure({ - container: { - enabled: false, - autoDiscover: false, - }, - }) - Router.reset() - Container.clear() - - app = express() - router = ExRouter() - app.use(express.json()) + let app: express.Application + let router: ExRouter + + beforeEach(() => { + Router.configure({ + container: { + enabled: false, + autoDiscover: false, + }, }) + Router.reset() + Container.clear() + + app = express() + router = ExRouter() + app.use(express.json()) + }) + + const setupApp = async (): Promise => { + Router.apply(router) + app.use(router) + } + + it('GET / should return 200', async () => { + Router.get('/directly', ({ res }) => res.send('Hello World')) + await setupApp() + await request(app).get('/directly').expect(200).expect('Hello World') + }) + + it('uses explicit container bindings when available', async () => { + class AuditService { + name = 'fallback' + } + + class BoundAuditService extends AuditService { + name = 'bound' + } - const setupApp = async (): Promise => { - Router.apply(router) - app.use(router) + class BoundUsersController { + @Bind(ClearRouterRequest, AuditService) + show(request: ClearRouterRequest, audit: AuditService) { + return { + id: request.param('id'), + audit: audit.name, + } + } } - it('GET / should return 200', async () => { - Router.get('/directly', ({ res }) => res.send('Hello World')) - await setupApp() - await request(app).get('/directly').expect(200).expect('Hello World') + Container.bind(AuditService, () => new BoundAuditService()) + Router.configure({ + container: { + enabled: true, + autoDiscover: true, + }, }) + Router.get('/api/bound/:id', [BoundUsersController, 'show']) - it('uses explicit container bindings when available', async () => { - class AuditService { - name = 'fallback' - } + await setupApp() - class BoundAuditService extends AuditService { - name = 'bound' - } + await request(app).get('/api/bound/321').expect(200).expect({ id: '321', audit: 'bound' }) + }) - class BoundUsersController { - @Bind(ClearRouterRequest, AuditService) - show (request: ClearRouterRequest, audit: AuditService) { - return { - id: request.param('id'), - audit: audit.name, - } - } + it('falls back to the default handler signature when binding is disabled', async () => { + class BoundUsersController { + @Bind(ClearRouterRequest) + show(ctx: any, request: ClearRouterRequest) { + return { + hasContext: Boolean(ctx.req), + id: request.param('id'), } + } + } - Container.bind(AuditService, () => new BoundAuditService()) - Router.configure({ - container: { - enabled: true, - autoDiscover: true, - }, - }) - Router.get('/api/bound/:id', [BoundUsersController, 'show']) - - await setupApp() - - await request(app) - .get('/api/bound/321') - .expect(200) - .expect({ id: '321', audit: 'bound' }) + Router.configure({ + container: { + enabled: false, + }, }) + Router.get('/api/default/:id', [BoundUsersController, 'show']) - it('falls back to the default handler signature when binding is disabled', async () => { - class BoundUsersController { - @Bind(ClearRouterRequest) - show (ctx: any, request: ClearRouterRequest) { - return { - hasContext: Boolean(ctx.req), - id: request.param('id'), - } - } - } + await setupApp() - Router.configure({ - container: { - enabled: false, - }, - }) - Router.get('/api/default/:id', [BoundUsersController, 'show']) + await request(app).get('/api/default/777').expect(200).expect({ hasContext: true, id: '777' }) + }) - await setupApp() - - await request(app) - .get('/api/default/777') - .expect(200) - .expect({ hasContext: true, id: '777' }) + it('should always expose req.getBody in handlers', async () => { + Router.get('/body-check', ({ req, res }) => { + res.json({ + hasGetBody: typeof req.getBody === 'function', + body: req.getBody(), + }) }) - it('should always expose req.getBody in handlers', async () => { - Router.get('/body-check', ({ req, res }) => { - res.json({ - hasGetBody: typeof req.getBody === 'function', - body: req.getBody(), - }) - }) - - Router.post('/body-check', ({ req, res }) => { - res.json({ - hasGetBody: typeof req.getBody === 'function', - body: req.getBody(), - }) - }) - - await setupApp() - - await request(app) - .get('/body-check') - .expect(200) - .expect({ - hasGetBody: true, - body: {}, - }) - - await request(app).post('/body-check').send({ foo: 'bar' }) - .expect(200) - .expect({ - hasGetBody: true, - body: { foo: 'bar' }, - }) + Router.post('/body-check', ({ req, res }) => { + res.json({ + hasGetBody: typeof req.getBody === 'function', + body: req.getBody(), + }) }) - it('supports POST _method override for PUT routes', async () => { - Router.put('/api/users/:id', ({ req, res }) => { - res.json({ - method: req.method, - id: req.params.id, - }) - }) - - await setupApp() - - await request(app) - .post('/api/users/123') - .send({ _method: 'PUT' }) - .expect(200) - .expect({ - method: 'PUT', - id: '123', - }) + await setupApp() + + await request(app).get('/body-check').expect(200).expect({ + hasGetBody: true, + body: {}, }) - it('returns 404 for POST to PUT route when _method override is missing', async () => { - Router.put('/api/users/:id', ({ res }) => res.send('updated')) + await request(app) + .post('/body-check') + .send({ foo: 'bar' }) + .expect(200) + .expect({ + hasGetBody: true, + body: { foo: 'bar' }, + }) + }) + + it('supports POST _method override for PUT routes', async () => { + Router.put('/api/users/:id', ({ req, res }) => { + res.json({ + method: req.method, + id: req.params.id, + }) + }) - await setupApp() + await setupApp() - await request(app) - .post('/api/users/123') - .send({}) - .expect(404) + await request(app).post('/api/users/123').send({ _method: 'PUT' }).expect(200).expect({ + method: 'PUT', + id: '123', }) + }) - it('sends actual data', async () => { - Router.post('/api/users/:id', ({ req }) => req.getBody()) + it('returns 404 for POST to PUT route when _method override is missing', async () => { + Router.put('/api/users/:id', ({ res }) => res.send('updated')) - await setupApp() + await setupApp() - await request(app) - .post('/api/users/123') - .send({ name: 'Jude' }) - .expect(201) - .expect({ name: 'Jude' }) - }) + await request(app).post('/api/users/123').send({}).expect(404) + }) + + it('sends actual data', async () => { + Router.post('/api/users/:id', ({ req }) => req.getBody()) + + await setupApp() + + await request(app).post('/api/users/123').send({ name: 'Jude' }).expect(201).expect({ name: 'Jude' }) + }) }) diff --git a/testsSetup.ts b/testsSetup.ts index ad61d3a0..19ca827f 100644 --- a/testsSetup.ts +++ b/testsSetup.ts @@ -17,5 +17,5 @@ const conf = { } globalThis.config ??= ((key: string, def?: string) => { - return key ? conf[key as never] ?? def : conf as never -}) as never \ No newline at end of file + return key ? (conf[key as never] ?? def) : (conf as never) +}) as never diff --git a/tsdown.config.ts b/tsdown.config.ts index 85dae01c..b34f94a6 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -6,85 +6,85 @@ import path from 'node:path' import { exists } from './utils/fs' export const baseConfig: UserConfig = { - dts: true, - clean: true, - shims: true, - unbundle: false, - entry: ['src/index.ts'], - format: ['esm', 'cjs'], - sourcemap: false, - exports: true, - outExtensions: (e) => { - return ({ - js: e.format === 'es' ? '.js' : '.cjs', - dts: '.d.ts' - }) - }, - hooks (hooks) { - hooks.hook('build:done', async (ctx) => { - try { - // Get the absolute output directory - const outDir = ctx.options.outDir ?? 'dist' + dts: true, + clean: true, + shims: true, + unbundle: false, + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + sourcemap: false, + exports: true, + outExtensions: (e) => { + return { + js: e.format === 'es' ? '.js' : '.cjs', + dts: '.d.ts', + } + }, + hooks(hooks) { + hooks.hook('build:done', async (ctx) => { + try { + // Get the absolute output directory + const outDir = ctx.options.outDir ?? 'dist' - // Get the absolute base directory - const base = ctx.options.cwd + // Get the absolute base directory + const base = ctx.options.cwd - // Make globale DTS partern - const gdts = path.join(base, 'src/*.d.ts') - // Make globale stubs partern - const stubs = path.join(base, 'src/**/*.stub') + // Make globale DTS partern + const gdts = path.join(base, 'src/*.d.ts') + // Make globale stubs partern + const stubs = path.join(base, 'src/**/*.stub') - for await (const entry of glob.stream([gdts, stubs])) { - const target = entry.replace('src', 'dist') + for await (const entry of glob.stream([gdts, stubs])) { + const target = entry.replace('src', 'dist') - // Ensure the target dir exists - if (await exists(entry) && !await exists(path.dirname(target))) - await mkdir(path.dirname(target)) - // Copy required files only for current package - if (await exists(entry) && entry.includes(ctx.options.cwd)) - copyFile(entry, target) - // Augment the d.ts file to the index.d.ts - if (entry.includes('.d.ts') && !entry.includes('node_modules') && !entry.includes('env.d.ts')) { - for await (const indexFile of glob.stream(path.join(outDir, 'index.d.*ts'))) { - const reference = `/// \n` - if (await exists(indexFile)) { - let content = await readFile(indexFile, 'utf8') - // Only add if it’s not already there - if (!content.includes(reference.trim())) { - content = reference + content - await writeFile(indexFile, content, 'utf8') - } - } - } - } + // Ensure the target dir exists + if ((await exists(entry)) && !(await exists(path.dirname(target)))) await mkdir(path.dirname(target)) + // Copy required files only for current package + if ((await exists(entry)) && entry.includes(ctx.options.cwd)) copyFile(entry, target) + // Augment the d.ts file to the index.d.ts + if (entry.includes('.d.ts') && !entry.includes('node_modules') && !entry.includes('env.d.ts')) { + for await (const indexFile of glob.stream(path.join(outDir, 'index.d.*ts'))) { + const reference = `/// \n` + if (await exists(indexFile)) { + let content = await readFile(indexFile, 'utf8') + // Only add if it’s not already there + if (!content.includes(reference.trim())) { + content = reference + content + await writeFile(indexFile, content, 'utf8') } - } catch { /** */ } - }) - }, - deps: { - skipNodeModulesBundle: true, - neverBundle: [ - 'fs', - 'os', - 'tsx', - 'path', - 'tsdown', - 'dotenv', - 'crypto', - 'rollup', - 'esbuild', - 'edge.js', - 'arkormx', - 'nodemailer', - 'typescript', - /^@h3ravel\/.*/gi, - /^@arkstack\/.*/gi, - /^node:.*/gi, - /.*\/promises$/gi, - 'fs-readdir-recursive', - '@app/models/User' - ] - }, + } + } + } + } + } catch { + /** */ + } + }) + }, + deps: { + skipNodeModulesBundle: true, + neverBundle: [ + 'fs', + 'os', + 'tsx', + 'path', + 'tsdown', + 'dotenv', + 'crypto', + 'rollup', + 'esbuild', + 'edge.js', + 'arkormx', + 'nodemailer', + 'typescript', + /^@h3ravel\/.*/gi, + /^@arkstack\/.*/gi, + /^node:.*/gi, + /.*\/promises$/gi, + 'fs-readdir-recursive', + '@app/models/User', + ], + }, } -export default defineConfig(baseConfig) \ No newline at end of file +export default defineConfig(baseConfig) diff --git a/utils/fs.ts b/utils/fs.ts index 734cdca9..734f2753 100644 --- a/utils/fs.ts +++ b/utils/fs.ts @@ -3,26 +3,28 @@ import path from 'node:path' import { stat } from 'node:fs/promises' export const findUpConfig = async (base: string, name: string, extensions: string[]) => { - return (await escalade(process.cwd(), async (_dir) => { - try { - for (const ext of extensions) { - const filename = path.join(_dir, base, name + '.' + ext) - if (await exists(filename)) { - return filename - } - } - } catch {/** */ } - -return '' - }))! -} - -export const exists = async (e: string) => { + return (await escalade(process.cwd(), async (_dir) => { try { - await stat(e) - -return true + for (const ext of extensions) { + const filename = path.join(_dir, base, name + '.' + ext) + if (await exists(filename)) { + return filename + } + } } catch { - return false + /** */ } -} \ No newline at end of file + + return '' + }))! +} + +export const exists = async (e: string) => { + try { + await stat(e) + + return true + } catch { + return false + } +} diff --git a/vitest.config.ts b/vitest.config.ts index 4ec10bfe..b480111c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,65 +4,65 @@ import path from 'node:path' import { pathToFileURL } from 'node:url' function appModelsResolver(): Plugin { - const modelPattern = /^@app\/models\/([^/]+)$/ + const modelPattern = /^@app\/models\/([^/]+)$/ - return { - name: 'resolve-app-models', - enforce: 'pre', + return { + name: 'resolve-app-models', + enforce: 'pre', - resolveId(source, importer) { - const match = source.match(modelPattern) + resolveId(source, importer) { + const match = source.match(modelPattern) - if (!match) { - return null - } + if (!match) { + return null + } - const [, model] = match - const pkgRoot = (importer ?? '').split('/src/').at(0)! + const [, model] = match + const pkgRoot = (importer ?? '').split('/src/').at(0)! - return path.resolve( - pkgRoot, - 'src/Contracts', - `${model}.ts`, - ) - }, - } + return path.resolve(pkgRoot, 'src/Contracts', `${model}.ts`) + }, + } } export default defineConfig({ - plugins: [appModelsResolver()], - resolve: { - alias: [ - { - find: /^@arkstack\/([^/]+)\/(.+)$/, - replacement: path.resolve(__dirname, 'packages') + '/$1/src/$2', - }, - { - find: /^@arkstack\/([^/]+)$/, - replacement: path.resolve(__dirname, 'packages') + '/$1/src/index.ts', - }, - ], - tsconfigPaths: true, + plugins: [appModelsResolver()], + resolve: { + alias: [ + { + find: /^@arkstack\/([^/]+)\/(.+)$/, + replacement: path.resolve(__dirname, 'packages') + '/$1/src/$2', + }, + { + find: /^@arkstack\/([^/]+)$/, + replacement: path.resolve(__dirname, 'packages') + '/$1/src/index.ts', + }, + ], + tsconfigPaths: true, + }, + test: { + // setup file is at the root of the project, so we need to resolve it not from the current package, but from the root + setupFiles: [path.resolve(__dirname, 'tests/setup.ts'), pathToFileURL('testsSetup.ts').href], + root: './', + passWithNoTests: true, + environment: 'node', + include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + exclude: [...configDefaults.exclude, 'templates/**'], + env: { + NODE_ENV: 'test', + VERBOSITY: '0', + }, + coverage: { + reporter: ['text', 'json', 'html', 'lcov'], + reportsDirectory: 'coverage', + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', + '**/.arkstack/**', + ], }, - test: { - // setup file is at the root of the project, so we need to resolve it not from the current package, but from the root - setupFiles: [ - path.resolve(__dirname, 'tests/setup.ts'), - pathToFileURL('testsSetup.ts').href - ], - root: './', - passWithNoTests: true, - environment: 'node', - include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], - exclude: [...configDefaults.exclude, 'templates/**'], - env: { - NODE_ENV: 'test', - VERBOSITY: '0' - }, - coverage: { - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: 'coverage', - exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', '**/.arkstack/**'], - } - } + }, }) From d873ce55737c68cd148660751e2f41784ae41597 Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Sun, 11 Oct 2026 04:40:41 +0100 Subject: [PATCH 3/3] feat(prepare): update tsconfig handling to support async operations and enhance configuration for dependencies --- packages/console/src/prepare.ts | 2 +- .../console/src/prepare/BuildInterfaces.ts | 21 +++++++++++++++++-- packages/console/src/prepare/TSConfig.ts | 2 -- templates/express-inertia/tsconfig.json | 6 +----- templates/h3/tsconfig.json | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/console/src/prepare.ts b/packages/console/src/prepare.ts index dd842c32..0fac1717 100644 --- a/packages/console/src/prepare.ts +++ b/packages/console/src/prepare.ts @@ -23,7 +23,7 @@ if (!process.env.NODE_CI) { BuildInterfaces.env() } -BuildInterfaces.tsconfig() +await BuildInterfaces.tsconfig() await ArkormArtifacts.sync() const LOG_LEVEL = parseInt(process.env.VERBOSITY ?? '0') > 0 ? [] : ['--log-level=silent'] diff --git a/packages/console/src/prepare/BuildInterfaces.ts b/packages/console/src/prepare/BuildInterfaces.ts index 7b2e0388..126b1291 100644 --- a/packages/console/src/prepare/BuildInterfaces.ts +++ b/packages/console/src/prepare/BuildInterfaces.ts @@ -4,6 +4,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' import { Arkstack } from '@arkstack/contract' import path from 'node:path' +import { readFile } from 'node:fs/promises' export class BuildInterfaces { private static project: Project @@ -22,14 +23,30 @@ export class BuildInterfaces { writeFileSync(path.join(Arkstack.rootDir(), '.arkstack/ark.d.ts'), declaration, 'utf8') } - static tsconfig() { + static async tsconfig() { + const pkg = JSON.parse(await readFile(path.join(process.cwd(), 'package.json'), 'utf-8')) + const deps = Object + .keys(pkg.dependencies ?? {}) + .concat(Object.keys(pkg.devDependencies ?? {})) + + if (deps.includes('@arkstack/inertia')) { + TSConfig['include'] = [...TSConfig['include'], '../resources'] + } + + if (deps.includes('react')) { + TSConfig['compilerOptions'] = { + ...TSConfig['compilerOptions'], + jsx: 'react-jsx' + } as never + } + const configs = { '.arkstack/tsconfig.json': JSON.stringify(TSConfig, undefined, 2), 'tsconfig.json': JSON.stringify(BaseTCConfig, undefined, 2), } for (const [file, config] of Object.entries(configs)) - writeFileSync(path.join(Arkstack.rootDir(), file), config, 'utf8') + writeFileSync(path.join(Arkstack.rootDir(), file), config + '\n', 'utf8') } /** diff --git a/packages/console/src/prepare/TSConfig.ts b/packages/console/src/prepare/TSConfig.ts index fdd538fe..27b69686 100644 --- a/packages/console/src/prepare/TSConfig.ts +++ b/packages/console/src/prepare/TSConfig.ts @@ -1,7 +1,5 @@ export const TSConfig = { compilerOptions: { - // baseUrl: '..', - // ignoreDeprecations: '6.0', module: 'es2022', target: 'es2022', lib: ['es2022', 'DOM'], diff --git a/templates/express-inertia/tsconfig.json b/templates/express-inertia/tsconfig.json index 398f2802..5722af5c 100644 --- a/templates/express-inertia/tsconfig.json +++ b/templates/express-inertia/tsconfig.json @@ -1,7 +1,3 @@ { - "extends": "./.arkstack/tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - }, - "include": [".arkstack/*.d.ts", "src", "tests", "resources"] + "extends": "./.arkstack/tsconfig.json" } diff --git a/templates/h3/tsconfig.json b/templates/h3/tsconfig.json index 5722af5c..5bc93038 100644 --- a/templates/h3/tsconfig.json +++ b/templates/h3/tsconfig.json @@ -1,3 +1,3 @@ { "extends": "./.arkstack/tsconfig.json" -} +} \ No newline at end of file