diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..7e4404ef --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +node_modules +dist +build +coverage +.turbo +pnpm-lock.yaml +**/database/migrations +**/src/database/migrations +**/.arkormx +**/storage diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..60d04444 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "plugins": ["prettier-plugin-brace-style"], + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2, + "braceStyle": "1tbs" +} 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/README.md b/README.md index 497dade0..827e9f08 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Arkstack uses structured error classes and centralized error middleware. Example: ```ts -throw new RequestError('Profile not found', 404); +throw new RequestError('Profile not found', 404) ``` All errors return consistent JSON: @@ -173,7 +173,7 @@ Standardized API responses: return new UserResource(req, res, user).json().status(200).additional({ status: 'success', message: 'User retrieved', -}); +}) ``` Response format: diff --git a/arkormx.config.ts b/arkormx.config.ts index 67c3047b..9eb7dd25 100644 --- a/arkormx.config.ts +++ b/arkormx.config.ts @@ -8,24 +8,24 @@ import { createArkormCurrentPageResolver } from 'resora' import path from 'node:path' const db = new Kysely>({ - dialect: new PostgresDialect({ - pool: new Pool({ - connectionString: process.env.DATABASE_URL, - }), + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL, }), + }), }) export default defineConfig({ - paths: { - models: path.join(process.cwd(), '/database/models'), - factories: path.join(process.cwd(), '/database/factories'), - seeders: path.join(process.cwd(), '/database/seeders'), - migrations: path.join(process.cwd(), '/database/migrations'), - buildOutput: './dist', - }, - outputExt: 'ts', - adapter: createKyselyAdapter(db), - pagination: { - resolveCurrentPage: createArkormCurrentPageResolver(), - }, -}) \ No newline at end of file + paths: { + models: path.join(process.cwd(), '/database/models'), + factories: path.join(process.cwd(), '/database/factories'), + seeders: path.join(process.cwd(), '/database/seeders'), + migrations: path.join(process.cwd(), '/database/migrations'), + buildOutput: './dist', + }, + outputExt: 'ts', + adapter: createKyselyAdapter(db), + pagination: { + resolveCurrentPage: createArkormCurrentPageResolver(), + }, +}) diff --git a/create-arkstack/cleaner.ts b/create-arkstack/cleaner.ts index f2145dd5..86ed48f0 100644 --- a/create-arkstack/cleaner.ts +++ b/create-arkstack/cleaner.ts @@ -3,35 +3,36 @@ import { existsSync, readFileSync } from 'node:fs' import { cwd } from 'node:process' import { join } from 'node:path' -const filesToPatch = [ - 'src/core/app.ts', -] +const filesToPatch = ['src/core/app.ts'] for (const file of filesToPatch) { - const filePath = join(cwd(), '../express', file) - if (!existsSync(filePath)) { - continue - } + const filePath = join(cwd(), '../express', file) + if (!existsSync(filePath)) { + continue + } - let content = readFileSync(filePath, 'utf-8') + let content = readFileSync(filePath, 'utf-8') - content = content - .replace('import { ModelNotFoundException } from \'arkormx\'\n', '') - .replace('import { prisma } from \'src/core/database\'\n', '') - .replace('import { Prisma } from \'@prisma/client\'\n', '') - .replace(' async shutdown () {\n await prisma.$disconnect()\n process.exit(0)\n }', ' async shutdown () {\n process.exit(0)\n }') - .replace( - ' * Shuts down the application by disconnecting from the database and exiting the process.', - ' * Shuts down the application and exits the process.', - ) - .replace( - /\n\s*if \((?:err|cause) instanceof Prisma\.PrismaClientKnownRequestError && (?:err|cause)\.code === "P2025"\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.meta\?\.modelName\} not found!`\n\s*\}\n/g, - '\n', - ) - .replace( - /\n\s*if \((?:err|cause) instanceof ModelNotFoundException\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.getModelName\(\)\} not found!`\n\s*\}\n/g, - '\n', - ) + content = content + .replace("import { ModelNotFoundException } from 'arkormx'\n", '') + .replace("import { prisma } from 'src/core/database'\n", '') + .replace("import { Prisma } from '@prisma/client'\n", '') + .replace( + ' async shutdown () {\n await prisma.$disconnect()\n process.exit(0)\n }', + ' async shutdown () {\n process.exit(0)\n }', + ) + .replace( + ' * Shuts down the application by disconnecting from the database and exiting the process.', + ' * Shuts down the application and exits the process.', + ) + .replace( + /\n\s*if \((?:err|cause) instanceof Prisma\.PrismaClientKnownRequestError && (?:err|cause)\.code === "P2025"\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.meta\?\.modelName\} not found!`\n\s*\}\n/g, + '\n', + ) + .replace( + /\n\s*if \((?:err|cause) instanceof ModelNotFoundException\) \{\n\s*error\.code = 404\n\s*error\.message = `\$\{(?:err|cause)\.getModelName\(\)\} not found!`\n\s*\}\n/g, + '\n', + ) - console.log(content) -} \ No newline at end of file + console.log(content) +} diff --git a/create-arkstack/src/Commands/CreateArkstackCommand.ts b/create-arkstack/src/Commands/CreateArkstackCommand.ts index 72791203..12c65514 100644 --- a/create-arkstack/src/Commands/CreateArkstackCommand.ts +++ b/create-arkstack/src/Commands/CreateArkstackCommand.ts @@ -19,8 +19,8 @@ export class CreateArkstackCommand extends Command { {--i|install: Install node_modules right away} {--t|token?: Kit repo authentication token.} {--d|desc?: Project Description.} - {--k|kit?: Runtime template. [${templates.map(e => e.alias).join(',')}]} - {--s|stack?: Inertia front-end stack. [${['none', ...inertiaStacks.map(e => e.value)].join(',')}]} + {--k|kit?: Runtime template. [${templates.map((e) => e.alias).join(',')}]} + {--s|stack?: Inertia front-end stack. [${['none', ...inertiaStacks.map((e) => e.value)].join(',')}]} {--l|lean: Make a lean project.} {--f|full: Make a full project (Ignored if lean is defined).} {--p|pre: Download prerelease version if available.} @@ -113,7 +113,10 @@ export class CreateArkstackCommand extends Command { message: 'Add an Inertia front-end? (server-side adapter + client scaffolding):', choices: [ { name: 'None - plain server-rendered views', value: 'none' }, - ...inertiaStacks.map((e) => ({ name: `${e.name} - Inertia + ${e.name}`, value: e.value })), + ...inertiaStacks.map((e) => ({ + name: `${e.name} - Inertia + ${e.name}`, + value: e.value, + })), ], default: 'none', when: () => !options.stack, @@ -205,20 +208,14 @@ export class CreateArkstackCommand extends Command { } if (kit.lean) { - spinner.info(Logger.parse([[ - 'Applying lean profile...', - 'green', - ]], '', false)).start() + spinner.info(Logger.parse([['Applying lean profile...', 'green']], '', false)).start() await actions.makeLeanProfile(kitName) } else { await actions.makeFullProfile(kitName) } if (wantsInertia) { - spinner.info(Logger.parse([[ - `Adding Inertia (${Str.title(stack)})...`, - 'green', - ]], '', false)).start() + spinner.info(Logger.parse([[`Adding Inertia (${Str.title(stack)})...`, 'green']], '', false)).start() await actions.applyInertia(kitName, stack) } diff --git a/create-arkstack/src/actions.ts b/create-arkstack/src/actions.ts index b904c70d..7d39fb8c 100644 --- a/create-arkstack/src/actions.ts +++ b/create-arkstack/src/actions.ts @@ -78,16 +78,16 @@ export default class { } /** - * Installs the project dependencies using the detected package manager. - * If a specific package name is provided, it will install that package + * Installs the project dependencies using the detected package manager. + * If a specific package name is provided, it will install that package * instead of all dependencies. - * - * @param name - * @param args - * @returns + * + * @param name + * @param args + * @returns */ async installPackage(name?: string, args: string[] = []) { - const bcmd = await Resolver.getPakageInstallCommand() + (name ? ` ${name}` : '') + const bcmd = (await Resolver.getPakageInstallCommand()) + (name ? ` ${name}` : '') const cmd = bcmd?.split(' ')[0] if (bcmd.includes(' ')) { args.unshift(...bcmd.split(' ').slice(1)) @@ -108,7 +108,7 @@ export default class { async complete(install = false) { let installed = false if (install) { - installed = await this.installPackage() === 0 + installed = (await this.installPackage()) === 0 } console.log('') @@ -123,11 +123,7 @@ export default class { Logger.success('Your Arkstack project has been created successfully') Logger.parse( - [ - ['cd', 'cyan'], - [installPath, 'yellow'], - installPath === process.cwd() ? ['✔', 'green'] : ['', 'green'], - ], + [['cd', 'cyan'], [installPath, 'yellow'], installPath === process.cwd() ? ['✔', 'green'] : ['', 'green']], ' ', ) @@ -180,9 +176,7 @@ export default class { const envPath = join(this.location!, '.env') const exampleEnvPath = join(this.location!, '.env.example') - const allowed = scope === 'max' - ? [...environment.max, ...environment.min] - : environment.min + const allowed = scope === 'max' ? [...environment.max, ...environment.min] : environment.min if (existsSync(exampleEnvPath)) { const env = await readFile(exampleEnvPath, 'utf-8') @@ -215,8 +209,7 @@ export default class { } async saveProfile() { - if (this.pkgPath) - await writeFile(this.pkgPath, JSON.stringify(this.packageJson, null, 2)) + if (this.pkgPath) await writeFile(this.pkgPath, JSON.stringify(this.packageJson, null, 2)) } async makeProfile() { @@ -226,14 +219,10 @@ export default class { this.packageJson = await readFile(pkgPath, 'utf-8').then(JSON.parse) for (const [name] of Object.entries(this.packageJson.dependencies)) { - if (/^@(arkstack|h3ravel)\//.test(name)) - delete this.packageJson.dependencies[name] + if (/^@(arkstack|h3ravel)\//.test(name)) delete this.packageJson.dependencies[name] } - const deps = Object.fromEntries([ - ...Object.entries(depsList), - ...Object.entries(this.packageJson.dependencies) - ]) + const deps = Object.fromEntries([...Object.entries(depsList), ...Object.entries(this.packageJson.dependencies)]) this.packageJson.dependencies = deps @@ -272,9 +261,7 @@ export default class { * @param catalogName The named catalog (empty string for the default catalog). */ private async resolveCatalogVersion(name: string, catalogName: string): Promise { - const fromCatalog = catalogName - ? catalogs[catalogName]?.[name] - : catalog[name] + const fromCatalog = catalogName ? catalogs[catalogName]?.[name] : catalog[name] const resolved = depsList[name] ?? fromCatalog @@ -313,7 +300,7 @@ export default class { return undefined } - const data = await response.json() as { version?: string } + const data = (await response.json()) as { version?: string } return data.version } catch { @@ -345,11 +332,7 @@ export default class { } } - const filesToPatch = [ - 'src/core/app.ts', - 'src/core/router.ts', - 'src/core/bootstrap.ts', - ] + const filesToPatch = ['src/core/app.ts', 'src/core/router.ts', 'src/core/bootstrap.ts'] for (const file of filesToPatch) { const filePath = join(this.location!, file) @@ -364,12 +347,15 @@ export default class { .replace(/import '@arkstack\/auth\/setup'\s*/g, '') .replace(/import '@arkstack\/queue\/setup'\s*/g, '') .replace(/import '@arkstack\/cache\/setup'\s*/g, '') - .replace('import { ValidatorDBDriver } from \'@arkstack/database\'\n', '') - .replace('import { ModelNotFoundException } from \'arkormx\'\n', '') - .replace('import { prisma } from \'src/core/database\'\n', '') - .replace('import { Prisma } from \'@prisma/client\'\n', '') + .replace("import { ValidatorDBDriver } from '@arkstack/database'\n", '') + .replace("import { ModelNotFoundException } from 'arkormx'\n", '') + .replace("import { prisma } from 'src/core/database'\n", '') + .replace("import { Prisma } from '@prisma/client'\n", '') .replace('Validator.useDatabase(new ValidatorDBDriver())', '') - .replace(' async shutdown () {\n await prisma.$disconnect()\n process.exit(0)\n }', ' async shutdown () {\n process.exit(0)\n }') + .replace( + ' async shutdown () {\n await prisma.$disconnect()\n process.exit(0)\n }', + ' async shutdown () {\n process.exit(0)\n }', + ) .replace( ' * Shuts down the application by disconnecting from the database and exiting the process.', ' * Shuts down the application and exits the process.', @@ -384,7 +370,7 @@ export default class { ) .replace( /if \(!\(err instanceof ValidationException\) &&\n\s*!\(err instanceof ModelNotFoundException\)\) {/g, - 'if (!(err instanceof ValidationException)) {' + 'if (!(err instanceof ValidationException)) {', ) .replace( /\s*\/\/ Register API routes\s*await ClearRouter\.group\('\/api', async \(\) => \{\s*await importFile\(join\(process\.cwd\(\), 'src\/routes\/api\.ts'\)\)\s*\}\)\s*/g, @@ -464,11 +450,11 @@ export default class { await this.saveProfile() } - /** - * Copy the shared + stack-specific stubs into the project, applying the - * same transforms publish does. - * - * @param spec + /** + * Copy the shared + stack-specific stubs into the project, applying the + * same transforms publish does. + * + * @param spec */ private async writeInertiaFiles(spec: InertiaStackSpec) { for (const file of [...sharedInertiaFiles, ...spec.files]) { @@ -543,12 +529,12 @@ export default class { } } - /** - * Register the `inertia()` middleware (after `resora()`) in the - * runtime's middleware config. - * - * @param _kit - * @returns + /** + * Register the `inertia()` middleware (after `resora()`) in the + * runtime's middleware config. + * + * @param _kit + * @returns */ private async patchInertiaMiddleware(_kit: KitName) { const file = join(this.location!, 'src/config/middleware.ts') @@ -570,10 +556,10 @@ export default class { await writeFile(file, content, 'utf-8') } - /** - * Point the root route at an Inertia page instead of the welcome view. - * - * @returns + /** + * Point the root route at an Inertia page instead of the welcome view. + * + * @returns */ private async patchInertiaWeb() { const file = join(this.location!, 'src/routes/web.ts') @@ -583,18 +569,18 @@ export default class { } const content = (await readFile(file, 'utf-8')) - .replace('import { view } from \'@arkstack/view\'', 'import { inertia } from \'@arkstack/inertia\'') - .replace(/await view\(\s*'welcome'/, 'await inertia(\'Index\'') + .replace("import { view } from '@arkstack/view'", "import { inertia } from '@arkstack/inertia'") + .replace(/await view\(\s*'welcome'/, "await inertia('Index'") await writeFile(file, content, 'utf-8') } - /** - * Enable JSX (React) and bring `resources/` into the TS program for the client code. - * - * @param spec - * @returns - */ + /** + * Enable JSX (React) and bring `resources/` into the TS program for the client code. + * + * @param spec + * @returns + */ private async patchInertiaTsConfig(spec: InertiaStackSpec) { const file = join(this.location!, 'tsconfig.json') @@ -628,17 +614,14 @@ export default class { pkg.scripts.build = 'ark build' pkg.scripts.postinstall = 'prepare' - pkg.name = Str.slugify( - this.appName ?? basename(this.location!).replace('.', ''), '-' - ) + pkg.name = Str.slugify(this.appName ?? basename(this.location!).replace('.', ''), '-') if (this.description) { pkg.description = this.description } for (const name of Object.keys(pkg.dependencies)) { - if (name.includes('@arkstack/driver') && name !== '@arkstack/driver-' + kit) - delete pkg.dependencies[name] + if (name.includes('@arkstack/driver') && name !== '@arkstack/driver-' + kit) delete pkg.dependencies[name] } this.packageJson = pkg diff --git a/create-arkstack/src/catalog.ts b/create-arkstack/src/catalog.ts index bb535e95..fb83aba2 100644 --- a/create-arkstack/src/catalog.ts +++ b/create-arkstack/src/catalog.ts @@ -1,29 +1,28 @@ // AUTO-GENERATED from pnpm-workspace.yaml by scripts/generate-catalog.mjs. // Do not edit by hand — run `pnpm generate:catalog` (also runs on build/test). -/* eslint-disable */ /** The default workspace catalog (`catalog:`). */ export const catalog: Record = { - "@h3ravel/musket": "^2.2.13", - "@h3ravel/shared": "^2.2.7", - "@h3ravel/support": "^2.2.7", - "@h3ravel/collect.js": "^5.4.0", - "@faker-js/faker": "^10.4.0", - "@arkormx/plugin-clear-router": "^0.1.57", - "@resora/plugin-clear-router": "^1.0.72", - "@kanun-hq/plugin-file": "^0.1.12", - "@kanun-hq/plugin-phone": "^0.1.8", - "arkormx": "^2.12.8", - "clear-router": "^2.9.3", - "dotenv": "^17.4.2", - "express": "^5.2.1", - "h3": "^2.0.1-rc.22", - "kysely": "^0.29.2", - "kanun": "^1.2.0", - "parasito": "^0.2.6", - "pg": "^8.22.0", - "resora": "^1.3.34", - "tsdown": "^0.22.14" + '@h3ravel/musket': '^2.2.13', + '@h3ravel/shared': '^2.2.7', + '@h3ravel/support': '^2.2.7', + '@h3ravel/collect.js': '^5.4.0', + '@faker-js/faker': '^10.4.0', + '@arkormx/plugin-clear-router': '^0.1.57', + '@resora/plugin-clear-router': '^1.0.72', + '@kanun-hq/plugin-file': '^0.1.12', + '@kanun-hq/plugin-phone': '^0.1.8', + arkormx: '^2.12.8', + 'clear-router': '^2.9.3', + dotenv: '^17.4.2', + express: '^5.2.1', + h3: '^2.0.1-rc.22', + kysely: '^0.29.2', + kanun: '^1.2.0', + parasito: '^0.2.6', + pg: '^8.22.0', + resora: '^1.3.34', + tsdown: '^0.22.14', } /** Named workspace catalogs (`catalog:`). */ diff --git a/create-arkstack/src/data.ts b/create-arkstack/src/data.ts index ebe7d90e..d6fdbaaf 100644 --- a/create-arkstack/src/data.ts +++ b/create-arkstack/src/data.ts @@ -1,119 +1,119 @@ export const filesToRemove = [ - 'src/app', - 'src/app/http/controllers', - 'src/app/http/resources', - 'src/models', - 'src/app/models', - 'database', - 'src/routes/api.ts', - 'src/database', - 'src/core/database.ts', - 'src/config/filesystem.ts', - 'src/config/notifications.ts', - 'src/config/database.ts', - 'src/config/resources.ts', - 'src/config/cache.ts', - 'src/config/queue.ts', - 'prisma', - 'prisma.config.ts', - 'arkorm.config.ts', - 'arkormx.config.ts', - 'arkorm.config.js', - 'arkormx.config.js', - 'arkorm.config.mjs', - 'arkormx.config.mjs', + 'src/app', + 'src/app/http/controllers', + 'src/app/http/resources', + 'src/models', + 'src/app/models', + 'database', + 'src/routes/api.ts', + 'src/database', + 'src/core/database.ts', + 'src/config/filesystem.ts', + 'src/config/notifications.ts', + 'src/config/database.ts', + 'src/config/resources.ts', + 'src/config/cache.ts', + 'src/config/queue.ts', + 'prisma', + 'prisma.config.ts', + 'arkorm.config.ts', + 'arkormx.config.ts', + 'arkorm.config.js', + 'arkormx.config.js', + 'arkorm.config.mjs', + 'arkormx.config.mjs', ] export const fullDependencies = [ - '@types/pg', - '@prisma/client', - '@prisma/adapter-pg', - '@arkstack/auth', - '@arkstack/cache', - '@arkstack/database', - '@arkstack/filesystem', - '@arkstack/jobs', - '@arkstack/notifications', - '@arkstack/queue', - 'pg', - 'kysely', - 'prisma', - 'arkormx', + '@types/pg', + '@prisma/client', + '@prisma/adapter-pg', + '@arkstack/auth', + '@arkstack/cache', + '@arkstack/database', + '@arkstack/filesystem', + '@arkstack/jobs', + '@arkstack/notifications', + '@arkstack/queue', + 'pg', + 'kysely', + 'prisma', + 'arkormx', ] export const leanDependencies = [ - // '@arkstack/console-slim', Depracated + // '@arkstack/console-slim', Depracated ] export const depsList: Record = { - '@arkstack/auth': '^0.19.2', - '@arkstack/cache': '^0.19.2', - '@arkstack/common': '^0.19.2', - '@arkstack/console': '^0.19.2', - '@arkstack/foundry': '^0.19.2', - '@arkstack/jobs': '^0.19.2', - // '@arkstack/console-slim': '^0.19.2', Depracated - '@arkstack/contract': '^0.19.2', - '@arkstack/database': '^0.19.2', - '@arkstack/driver-express': '^0.19.2', - '@arkstack/driver-h3': '^0.19.2', - '@arkstack/filesystem': '^0.19.2', - '@arkstack/http': '^0.19.2', - '@arkstack/view': '^0.19.2', - '@arkstack/queue': '^0.19.2', - '@arkstack/scheduler': '^0.19.2', - '@arkstack/notifications': '^0.19.2', - '@h3ravel/musket': '^2.2.13', - '@h3ravel/support': '^2.2.7', + '@arkstack/auth': '^0.19.2', + '@arkstack/cache': '^0.19.2', + '@arkstack/common': '^0.19.2', + '@arkstack/console': '^0.19.2', + '@arkstack/foundry': '^0.19.2', + '@arkstack/jobs': '^0.19.2', + // '@arkstack/console-slim': '^0.19.2', Depracated + '@arkstack/contract': '^0.19.2', + '@arkstack/database': '^0.19.2', + '@arkstack/driver-express': '^0.19.2', + '@arkstack/driver-h3': '^0.19.2', + '@arkstack/filesystem': '^0.19.2', + '@arkstack/http': '^0.19.2', + '@arkstack/view': '^0.19.2', + '@arkstack/queue': '^0.19.2', + '@arkstack/scheduler': '^0.19.2', + '@arkstack/notifications': '^0.19.2', + '@h3ravel/musket': '^2.2.13', + '@h3ravel/support': '^2.2.7', } export const environment = { - min: [ - 'PORT', - 'HOST', - 'APP_URL', - 'APP_KEY', - 'APP_ENV', - 'APP_HOST', - 'APP_PORT', - 'APP_NAME', - 'APP_DEBUG', - 'APP_LOCALE', - 'APP_TIMEZONE', - 'NODE_ENV', - 'FRONTEND_URL', - 'NGROK_DOMAIN', - 'NGROK_AUTHTOKEN', - 'APP_FAKER_LOCALE', - 'APP_FALLBACK_LOCALE' - ], - max: [ - 'FILESYSTEM_DISK', - 'CACHE_STORE', - 'QUEUE_CONNECTION', - 'REDIS_HOST', - 'REDIS_PORT', - 'REDIS_PASSWORD', - 'JWT_EXPIRES_IN', - 'DATABASE_URL', - 'DB_CONNECTION', - 'DB_HOST', - 'DB_PORT', - 'DB_DATABASE', - 'DB_USERNAME', - 'DB_PASSWORD', - 'MAIL_HOST', - 'MAIL_PORT', - 'MAIL_SECURE', - 'MAIL_USERNAME', - 'MAIL_PASSWORD', - 'MAIL_FROM_ADDRESS', - 'MAIL_TEST_ADDRESS', - 'AWS_ACCESS_KEY_ID', - 'AWS_SECRET_ACCESS_KEY', - 'AWS_DEFAULT_REGION', - 'AWS_BUCKET', - 'AWS_URL', - 'AWS_ENDPOINT', - ], -} \ No newline at end of file + min: [ + 'PORT', + 'HOST', + 'APP_URL', + 'APP_KEY', + 'APP_ENV', + 'APP_HOST', + 'APP_PORT', + 'APP_NAME', + 'APP_DEBUG', + 'APP_LOCALE', + 'APP_TIMEZONE', + 'NODE_ENV', + 'FRONTEND_URL', + 'NGROK_DOMAIN', + 'NGROK_AUTHTOKEN', + 'APP_FAKER_LOCALE', + 'APP_FALLBACK_LOCALE', + ], + max: [ + 'FILESYSTEM_DISK', + 'CACHE_STORE', + 'QUEUE_CONNECTION', + 'REDIS_HOST', + 'REDIS_PORT', + 'REDIS_PASSWORD', + 'JWT_EXPIRES_IN', + 'DATABASE_URL', + 'DB_CONNECTION', + 'DB_HOST', + 'DB_PORT', + 'DB_DATABASE', + 'DB_USERNAME', + 'DB_PASSWORD', + 'MAIL_HOST', + 'MAIL_PORT', + 'MAIL_SECURE', + 'MAIL_USERNAME', + 'MAIL_PASSWORD', + 'MAIL_FROM_ADDRESS', + 'MAIL_TEST_ADDRESS', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_DEFAULT_REGION', + 'AWS_BUCKET', + 'AWS_URL', + 'AWS_ENDPOINT', + ], +} diff --git a/create-arkstack/src/inertia.ts b/create-arkstack/src/inertia.ts index fe7b7a56..f8d4edc2 100644 --- a/create-arkstack/src/inertia.ts +++ b/create-arkstack/src/inertia.ts @@ -21,7 +21,10 @@ export const sharedInertiaFiles: InertiaFile[] = [ { from: 'config/inertia.ts.stub', to: 'src/config/inertia.ts' }, { from: 'config/app.css.stub', to: 'resources/css/app.css' }, { from: 'views/app.edge.stub', to: 'src/resources/views/app.edge' }, - { from: 'shared/resources/js/vite-env.d.ts.stub', to: 'resources/js/vite-env.d.ts' }, + { + from: 'shared/resources/js/vite-env.d.ts.stub', + to: 'resources/js/vite-env.d.ts', + }, ] /** Server-side dependencies shared by every stack. */ @@ -55,7 +58,10 @@ export const inertiaStacks: InertiaStackSpec[] = [ files: [ { from: 'react/resources/js/app.tsx.stub', to: 'resources/js/app.tsx' }, { from: 'react/vite.config.ts.stub', to: 'vite.config.ts' }, - { from: 'react/resources/js/Pages/Index.tsx.stub', to: 'resources/js/Pages/Index.tsx' }, + { + from: 'react/resources/js/Pages/Index.tsx.stub', + to: 'resources/js/Pages/Index.tsx', + }, ], }, { @@ -73,7 +79,10 @@ export const inertiaStacks: InertiaStackSpec[] = [ files: [ { from: 'vue/resources/js/app.ts.stub', to: 'resources/js/app.ts' }, { from: 'vue/vite.config.ts.stub', to: 'vite.config.ts' }, - { from: 'vue/resources/js/Pages/Index.vue.stub', to: 'resources/js/Pages/Index.vue' }, + { + from: 'vue/resources/js/Pages/Index.vue.stub', + to: 'resources/js/Pages/Index.vue', + }, ], }, { @@ -91,16 +100,19 @@ export const inertiaStacks: InertiaStackSpec[] = [ files: [ { from: 'svelte/resources/js/app.ts.stub', to: 'resources/js/app.ts' }, { from: 'svelte/vite.config.ts.stub', to: 'vite.config.ts' }, - { from: 'svelte/resources/js/Pages/Index.svelte.stub', to: 'resources/js/Pages/Index.svelte' }, + { + from: 'svelte/resources/js/Pages/Index.svelte.stub', + to: 'resources/js/Pages/Index.svelte', + }, ], }, ] -/** - * Look up a stack spec by its identifier. - * - * @param stack - * @returns +/** + * Look up a stack spec by its identifier. + * + * @param stack + * @returns */ export const findInertiaStack = (stack: string): InertiaStackSpec | undefined => inertiaStacks.find((s) => s.value === stack) @@ -112,9 +124,7 @@ export const findInertiaStack = (stack: string): InertiaStackSpec | undefined => * @param content The raw stub contents. * @param spec The selected stack. */ -export const transformInertiaStub = ( - content: string, - spec: InertiaStackSpec -): string => content - .replaceAll('{{ext}}', spec.ext) - .replaceAll('{{reactRefresh}}', spec.reactRefresh ? '@viteReactRefresh\n ' : '') +export const transformInertiaStub = (content: string, spec: InertiaStackSpec): string => + content + .replaceAll('{{ext}}', spec.ext) + .replaceAll('{{reactRefresh}}', spec.reactRefresh ? '@viteReactRefresh\n ' : '') diff --git a/create-arkstack/src/run.ts b/create-arkstack/src/run.ts index 3e59f605..d4ec45ac 100644 --- a/create-arkstack/src/run.ts +++ b/create-arkstack/src/run.ts @@ -4,11 +4,11 @@ import { CreateArkstackCommand } from './Commands/CreateArkstackCommand' import { Kernel } from '@h3ravel/musket' import { Logger } from '@h3ravel/shared' -class Application { } +class Application {} Kernel.init(new Application(), { rootCommand: CreateArkstackCommand, exceptionHandler: (e) => { Logger.error(`ERROR: ${e.message}`) - } + }, }) diff --git a/create-arkstack/src/templates.ts b/create-arkstack/src/templates.ts index 0ea8f58b..1d697f17 100644 --- a/create-arkstack/src/templates.ts +++ b/create-arkstack/src/templates.ts @@ -42,7 +42,6 @@ export const templates: Template[] = [ // }, ] - export const projectScopes = [ { name: 'Lean', @@ -54,4 +53,4 @@ export const projectScopes = [ hint: 'Full Arkstack experience on {template}', lean: false, }, -] \ No newline at end of file +] diff --git a/create-arkstack/src/types.ts b/create-arkstack/src/types.ts index 0b2f3e55..048edf0e 100644 --- a/create-arkstack/src/types.ts +++ b/create-arkstack/src/types.ts @@ -1,42 +1,40 @@ export type KitName = 'express' | 'h3' | 'fastify' | 'koa' | 'nest' | 'next' | 'nuxt' export interface Template { - name: string; - alias: 'express' | 'h3' | 'express-lean' | 'h3-lean'; - hint: string; - source: string; - lean?: boolean; - locked?: boolean; - baseAlias?: 'express' | 'h3'; - prereleaseSource?: string; + name: string + alias: 'express' | 'h3' | 'express-lean' | 'h3-lean' + hint: string + source: string + lean?: boolean + locked?: boolean + baseAlias?: 'express' | 'h3' + prereleaseSource?: string } - - /** Supported Inertia front-end stacks (plus `none` to opt out). */ export type InertiaStack = 'none' | 'react' | 'vue' | 'svelte' /** A single file copied from `@arkstack/inertia`'s stubs into the new project. */ export interface InertiaFile { - /** Path of the stub, relative to the package's `stubs/` directory. */ - from: string - /** Destination path, relative to the project root. */ - to: string + /** Path of the stub, relative to the package's `stubs/` directory. */ + from: string + /** Destination path, relative to the project root. */ + to: string } export interface InertiaStackSpec { - /** Display name shown in the prompt. */ - name: string - /** The stack identifier / prompt value. */ - value: Exclude - /** Client entry extension (`tsx` for React, otherwise `ts`). */ - ext: 'tsx' | 'ts' - /** Whether the root template needs the `@viteReactRefresh` preamble. */ - reactRefresh: boolean - /** Runtime dependencies this stack adds. */ - deps: Record - /** Dev dependencies this stack adds. */ - devDeps: Record - /** Stack-specific stub files (the shared files are added on top). */ - files: InertiaFile[] -} \ No newline at end of file + /** Display name shown in the prompt. */ + name: string + /** The stack identifier / prompt value. */ + value: Exclude + /** Client entry extension (`tsx` for React, otherwise `ts`). */ + ext: 'tsx' | 'ts' + /** Whether the root template needs the `@viteReactRefresh` preamble. */ + reactRefresh: boolean + /** Runtime dependencies this stack adds. */ + deps: Record + /** Dev dependencies this stack adds. */ + devDeps: Record + /** Stack-specific stub files (the shared files are added on top). */ + files: InertiaFile[] +} diff --git a/create-arkstack/src/utils.ts b/create-arkstack/src/utils.ts index 1d80dcc0..d4ec1e8e 100644 --- a/create-arkstack/src/utils.ts +++ b/create-arkstack/src/utils.ts @@ -8,7 +8,7 @@ import path from 'node:path' * * @param dirPath */ -export async function mountTemplatesDir (dirPath: string) { +export async function mountTemplatesDir(dirPath: string) { const templatesDir = path.join(dirPath, 'templates') const existingEntries = await readdir(dirPath) @@ -17,8 +17,8 @@ export async function mountTemplatesDir (dirPath: string) { await Promise.all( existingEntries - .filter(entry => entry !== 'templates') - .map(entry => rm(path.join(dirPath, entry), { recursive: true, force: true })) + .filter((entry) => entry !== 'templates') + .map((entry) => rm(path.join(dirPath, entry), { recursive: true, force: true })), ) await rm(templatesDir, { recursive: true, force: true }) @@ -30,7 +30,7 @@ export async function mountTemplatesDir (dirPath: string) { * @param dirPath * @param keepFileName */ -export async function cleanDirectoryExcept (dirPath: string, keepFileName: string) { +export async function cleanDirectoryExcept(dirPath: string, keepFileName: string) { const files = await readdir(dirPath) for (const file of files) { @@ -48,7 +48,7 @@ export async function cleanDirectoryExcept (dirPath: string, keepFileName: strin * @param dirPath * @param parent */ -export async function hoistDirectoryContents (parent: string, dirPath: string) { +export async function hoistDirectoryContents(parent: string, dirPath: string) { const source = path.isAbsolute(dirPath) ? dirPath : path.join(process.cwd(), dirPath) const targetParent = path.isAbsolute(parent) ? parent : path.join(process.cwd(), parent) diff --git a/create-arkstack/tests/catalog.test.ts b/create-arkstack/tests/catalog.test.ts index b0954612..0e847ff8 100644 --- a/create-arkstack/tests/catalog.test.ts +++ b/create-arkstack/tests/catalog.test.ts @@ -10,59 +10,59 @@ import { tmpdir } from 'node:os' const tempDirs: string[] = [] afterEach(async () => { - await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) - tempDirs.length = 0 + await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) + tempDirs.length = 0 }) const hasCatalogSpecifier = (deps: Record = {}) => - Object.values(deps).some((version) => version.startsWith('catalog:')) + Object.values(deps).some((version) => version.startsWith('catalog:')) describe('resolveCatalogDeps', () => { - test('rewrites catalog: specifiers using the workspace catalog snapshot', async () => { - const location = await mkdtemp(join(tmpdir(), 'create-arkstack-catalog-')) - tempDirs.push(location) + test('rewrites catalog: specifiers using the workspace catalog snapshot', async () => { + const location = await mkdtemp(join(tmpdir(), 'create-arkstack-catalog-')) + tempDirs.push(location) - await writeFile( - join(location, 'package.json'), - JSON.stringify( - { - dependencies: { - // Re-added from depsList, must not remain `catalog:`. - '@h3ravel/musket': 'catalog:', - // Not in depsList: resolved from the catalog snapshot. - dotenv: 'catalog:', - keep: '^1.0.0', - }, - devDependencies: { - // devDependencies are swept too. - dotenv: 'catalog:', - keepDev: '^1.0.0', - }, - }, - null, - 2, - ), - ) + await writeFile( + join(location, 'package.json'), + JSON.stringify( + { + dependencies: { + // Re-added from depsList, must not remain `catalog:`. + '@h3ravel/musket': 'catalog:', + // Not in depsList: resolved from the catalog snapshot. + dotenv: 'catalog:', + keep: '^1.0.0', + }, + devDependencies: { + // devDependencies are swept too. + dotenv: 'catalog:', + keepDev: '^1.0.0', + }, + }, + null, + 2, + ), + ) - const actions = new Actions(location) - await actions.makeProfile() - await actions.saveProfile() + const actions = new Actions(location) + await actions.makeProfile() + await actions.saveProfile() - const pkg = JSON.parse(await readFile(join(location, 'package.json'), 'utf-8')) + const pkg = JSON.parse(await readFile(join(location, 'package.json'), 'utf-8')) - // No catalog: specifier survives in either dependency set. - expect(hasCatalogSpecifier(pkg.dependencies)).toBe(false) - expect(hasCatalogSpecifier(pkg.devDependencies)).toBe(false) + // No catalog: specifier survives in either dependency set. + expect(hasCatalogSpecifier(pkg.dependencies)).toBe(false) + expect(hasCatalogSpecifier(pkg.devDependencies)).toBe(false) - // Unlisted catalog dep resolved from the snapshot. - expect(pkg.dependencies.dotenv).toBe(catalog.dotenv) - expect(pkg.devDependencies.dotenv).toBe(catalog.dotenv) + // Unlisted catalog dep resolved from the snapshot. + expect(pkg.dependencies.dotenv).toBe(catalog.dotenv) + expect(pkg.devDependencies.dotenv).toBe(catalog.dotenv) - // Curated depsList wins for framework packages. - expect(pkg.dependencies['@h3ravel/musket']).toBe(depsList['@h3ravel/musket']) + // Curated depsList wins for framework packages. + expect(pkg.dependencies['@h3ravel/musket']).toBe(depsList['@h3ravel/musket']) - // Untouched deps are preserved. - expect(pkg.dependencies.keep).toBe('^1.0.0') - expect(pkg.devDependencies.keepDev).toBe('^1.0.0') - }) + // Untouched deps are preserved. + expect(pkg.dependencies.keep).toBe('^1.0.0') + expect(pkg.devDependencies.keepDev).toBe('^1.0.0') + }) }) diff --git a/create-arkstack/tests/cmd.test.ts b/create-arkstack/tests/cmd.test.ts index a48fbd53..162e1ce6 100644 --- a/create-arkstack/tests/cmd.test.ts +++ b/create-arkstack/tests/cmd.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest' -describe('Support Package\'s Str', () => { - test('plural of user is users', () => { - expect(1).toBe(1) - }) -}) +describe("Support Package's Str", () => { + test('plural of user is users', () => { + expect(1).toBe(1) + }) +}) diff --git a/create-arkstack/tests/inertia.test.ts b/create-arkstack/tests/inertia.test.ts index 53409f09..4ad7e3f6 100644 --- a/create-arkstack/tests/inertia.test.ts +++ b/create-arkstack/tests/inertia.test.ts @@ -9,182 +9,192 @@ import { tmpdir } from 'node:os' const tempDirs: string[] = [] afterEach(async () => { - await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) - tempDirs.length = 0 + await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) + tempDirs.length = 0 }) /** Build a minimal downloaded-repo layout with the inertia stubs and an express template. */ const scaffold = async () => { - const repo = await mkdtemp(join(tmpdir(), 'create-arkstack-repo-')) - const project = await mkdtemp(join(tmpdir(), 'create-arkstack-proj-')) - tempDirs.push(repo, project) - - // Inertia stubs (subset) under the repo's package dir. - const stubs = join(repo, 'packages/inertia/stubs') - await mkdir(join(stubs, 'config'), { recursive: true }) - await mkdir(join(stubs, 'views'), { recursive: true }) - await mkdir(join(stubs, 'shared/resources/js'), { recursive: true }) - await mkdir(join(stubs, 'react/resources/js/Pages'), { recursive: true }) - - await writeFile(join(stubs, 'config/inertia.ts.stub'), 'export default () => ({ root_view: "app" })\n') - await writeFile(join(stubs, 'config/app.css.stub'), 'body { margin: 0 }\n') - await writeFile(join(stubs, 'views/app.edge.stub'), '@inertiaHead\n {{reactRefresh}}@vite([\'resources/js/app.{{ext}}\', \'resources/css/app.css\'])\n') - await writeFile(join(stubs, 'shared/resources/js/vite-env.d.ts.stub'), '/// \n') - await writeFile(join(stubs, 'react/resources/js/app.tsx.stub'), 'createInertiaApp({})\n') - await writeFile(join(stubs, 'react/vite.config.ts.stub'), 'export default {}\n') - await writeFile(join(stubs, 'react/resources/js/Pages/Index.tsx.stub'), 'export default function Index() { return null }\n') - - // Project files an express template would carry. - await mkdir(join(project, 'src/config'), { recursive: true }) - await mkdir(join(project, 'src/routes'), { recursive: true }) - - await writeFile( - join(project, 'src/config/middleware.ts'), - [ - 'import { formdata, requestLogger, resora } from \'@arkstack/driver-express/middlewares\'', - '', - 'export default () => ({', - ' before: [', - ' resora(),', - ' ],', - '})', - '', - ].join('\n'), - ) - await writeFile( - join(project, 'src/routes/web.ts'), - [ - 'import { Router } from \'@arkstack/driver-express\'', - 'import { view } from \'@arkstack/view\'', - '', - 'Router.get(\'/\', async () => {', - ' return await view(\'welcome\', { title: \'hi\' })', - '})', - '', - ].join('\n'), - ) - await writeFile(join(project, 'tsconfig.json'), JSON.stringify({ extends: './.arkstack/tsconfig.json' }, null, 2)) - await writeFile( - join(project, 'package.json'), - JSON.stringify({ dependencies: { '@arkstack/view': '^0.15.4' }, scripts: { dev: 'ark dev' } }, null, 2), - ) - - return { repo, project } + const repo = await mkdtemp(join(tmpdir(), 'create-arkstack-repo-')) + const project = await mkdtemp(join(tmpdir(), 'create-arkstack-proj-')) + tempDirs.push(repo, project) + + // Inertia stubs (subset) under the repo's package dir. + const stubs = join(repo, 'packages/inertia/stubs') + await mkdir(join(stubs, 'config'), { recursive: true }) + await mkdir(join(stubs, 'views'), { recursive: true }) + await mkdir(join(stubs, 'shared/resources/js'), { recursive: true }) + await mkdir(join(stubs, 'react/resources/js/Pages'), { recursive: true }) + + await writeFile(join(stubs, 'config/inertia.ts.stub'), 'export default () => ({ root_view: "app" })\n') + await writeFile(join(stubs, 'config/app.css.stub'), 'body { margin: 0 }\n') + await writeFile( + join(stubs, 'views/app.edge.stub'), + "@inertiaHead\n {{reactRefresh}}@vite(['resources/js/app.{{ext}}', 'resources/css/app.css'])\n", + ) + await writeFile(join(stubs, 'shared/resources/js/vite-env.d.ts.stub'), '/// \n') + await writeFile(join(stubs, 'react/resources/js/app.tsx.stub'), 'createInertiaApp({})\n') + await writeFile(join(stubs, 'react/vite.config.ts.stub'), 'export default {}\n') + await writeFile( + join(stubs, 'react/resources/js/Pages/Index.tsx.stub'), + 'export default function Index() { return null }\n', + ) + + // Project files an express template would carry. + await mkdir(join(project, 'src/config'), { recursive: true }) + await mkdir(join(project, 'src/routes'), { recursive: true }) + + await writeFile( + join(project, 'src/config/middleware.ts'), + [ + "import { formdata, requestLogger, resora } from '@arkstack/driver-express/middlewares'", + '', + 'export default () => ({', + ' before: [', + ' resora(),', + ' ],', + '})', + '', + ].join('\n'), + ) + await writeFile( + join(project, 'src/routes/web.ts'), + [ + "import { Router } from '@arkstack/driver-express'", + "import { view } from '@arkstack/view'", + '', + "Router.get('/', async () => {", + " return await view('welcome', { title: 'hi' })", + '})', + '', + ].join('\n'), + ) + await writeFile(join(project, 'tsconfig.json'), JSON.stringify({ extends: './.arkstack/tsconfig.json' }, null, 2)) + await writeFile( + join(project, 'package.json'), + JSON.stringify( + { + dependencies: { '@arkstack/view': '^0.15.4' }, + scripts: { dev: 'ark dev' }, + }, + null, + 2, + ), + ) + + return { repo, project } } describe('applyInertia', () => { - test('captures stubs, writes files, injects deps and patches the runtime (react)', async () => { - const { repo, project } = await scaffold() - - const actions = new Actions(project) - const captured = await actions.captureInertiaStubs(repo) - expect(captured).toBe(true) - - await actions.applyInertia('express', 'react') - - // Files written into the project. - expect(existsSync(join(project, 'src/config/inertia.ts'))).toBe(true) - expect(existsSync(join(project, 'resources/css/app.css'))).toBe(true) - expect(existsSync(join(project, 'resources/js/app.tsx'))).toBe(true) - expect(existsSync(join(project, 'resources/js/vite-env.d.ts'))).toBe(true) - expect(existsSync(join(project, 'resources/js/Pages/Index.tsx'))).toBe(true) - expect(existsSync(join(project, 'vite.config.ts'))).toBe(true) - - // Root template transforms: react ext + the React Refresh preamble. - const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') - expect(edge).toContain('@viteReactRefresh') - expect(edge).toContain('resources/js/app.tsx') - expect(edge).not.toContain('{{ext}}') - expect(edge).not.toContain('{{reactRefresh}}') - - // Dependencies + scripts. - const pkg = JSON.parse(await readFile(join(project, 'package.json'), 'utf-8')) - expect(pkg.dependencies['@arkstack/inertia']).toBeDefined() - expect(pkg.dependencies['@inertiajs/react']).toBeDefined() - expect(pkg.dependencies.react).toBeDefined() - expect(pkg.dependencies.vite).toBeDefined() - expect(pkg.devDependencies['@vitejs/plugin-react']).toBeDefined() - expect(pkg.scripts['dev:client']).toBe('vite') - - // Middleware registers inertia() after resora(). - const mw = await readFile(join(project, 'src/config/middleware.ts'), 'utf-8') - expect(mw).toContain('import { formdata, inertia, requestLogger, resora }') - expect(mw).toMatch(/resora\(\),\s*\n\s*inertia\(\),/) - - // Root route renders an Inertia page. - const web = await readFile(join(project, 'src/routes/web.ts'), 'utf-8') - expect(web).toContain('import { inertia } from \'@arkstack/inertia\'') - expect(web).toContain('inertia(\'Index\'') - expect(web).not.toContain('view(\'welcome\'') - - // tsconfig enables JSX and includes resources/. - const ts = JSON.parse(await readFile(join(project, 'tsconfig.json'), 'utf-8')) - expect(ts.compilerOptions.jsx).toBe('react-jsx') - expect(ts.include).toContain('resources') - }) - - test('vue stack does not enable jsx or the react preamble', async () => { - const { repo, project } = await scaffold() - - // Provide the vue stubs the spec references. - const stubs = join(repo, 'packages/inertia/stubs') - await mkdir(join(stubs, 'vue/resources/js/Pages'), { recursive: true }) - await writeFile(join(stubs, 'vue/resources/js/app.ts.stub'), 'createInertiaApp({})\n') - await writeFile(join(stubs, 'vue/vite.config.ts.stub'), 'export default {}\n') - await writeFile(join(stubs, 'vue/resources/js/Pages/Index.vue.stub'), '\n') - - const actions = new Actions(project) - await actions.captureInertiaStubs(repo) - await actions.applyInertia('express', 'vue') - - expect(existsSync(join(project, 'resources/js/app.ts'))).toBe(true) - expect(existsSync(join(project, 'resources/js/Pages/Index.vue'))).toBe(true) - - const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') - expect(edge).not.toContain('@viteReactRefresh') - expect(edge).toContain('resources/js/app.ts') - - const pkg = JSON.parse(await readFile(join(project, 'package.json'), 'utf-8')) - expect(pkg.dependencies['@inertiajs/vue3']).toBeDefined() - expect(pkg.devDependencies['@vitejs/plugin-vue']).toBeDefined() - - const ts = JSON.parse(await readFile(join(project, 'tsconfig.json'), 'utf-8')) - expect(ts.compilerOptions.jsx).toBeUndefined() - expect(ts.include).toContain('resources') - }) - - test('adds the React preamble even when the stub predates the {{reactRefresh}} placeholder', async () => { - const { repo, project } = await scaffold() - - // Simulate an older downloaded stub: no placeholder, hardcoded `app.ts`. - const stubs = join(repo, 'packages/inertia/stubs') - await writeFile( - join(stubs, 'views/app.edge.stub'), - '@inertiaHead\n @vite(\'resources/js/app.ts\')\n', - ) - - const actions = new Actions(project) - await actions.captureInertiaStubs(repo) - await actions.applyInertia('express', 'react') - - const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') - expect(edge).toContain('@viteReactRefresh') - // The preamble must sit immediately before the @vite tag. - expect(edge).toMatch(/@viteReactRefresh\n\s*@vite\(/) - // The entry extension is corrected to React's `tsx`. - expect(edge).toContain('resources/js/app.tsx') - expect(edge).not.toContain('resources/js/app.ts\'') - }) - - test('skips gracefully when stubs were never captured', async () => { - const project = await mkdtemp(join(tmpdir(), 'create-arkstack-proj-')) - tempDirs.push(project) - await writeFile(join(project, 'package.json'), JSON.stringify({ dependencies: {} })) - - const actions = new Actions(project) - // No captureInertiaStubs() call → nothing to apply. - await actions.applyInertia('express', 'react') - - expect(existsSync(join(project, 'resources/js/app.tsx'))).toBe(false) - }) + test('captures stubs, writes files, injects deps and patches the runtime (react)', async () => { + const { repo, project } = await scaffold() + + const actions = new Actions(project) + const captured = await actions.captureInertiaStubs(repo) + expect(captured).toBe(true) + + await actions.applyInertia('express', 'react') + + // Files written into the project. + expect(existsSync(join(project, 'src/config/inertia.ts'))).toBe(true) + expect(existsSync(join(project, 'resources/css/app.css'))).toBe(true) + expect(existsSync(join(project, 'resources/js/app.tsx'))).toBe(true) + expect(existsSync(join(project, 'resources/js/vite-env.d.ts'))).toBe(true) + expect(existsSync(join(project, 'resources/js/Pages/Index.tsx'))).toBe(true) + expect(existsSync(join(project, 'vite.config.ts'))).toBe(true) + + // Root template transforms: react ext + the React Refresh preamble. + const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') + expect(edge).toContain('@viteReactRefresh') + expect(edge).toContain('resources/js/app.tsx') + expect(edge).not.toContain('{{ext}}') + expect(edge).not.toContain('{{reactRefresh}}') + + // Dependencies + scripts. + const pkg = JSON.parse(await readFile(join(project, 'package.json'), 'utf-8')) + expect(pkg.dependencies['@arkstack/inertia']).toBeDefined() + expect(pkg.dependencies['@inertiajs/react']).toBeDefined() + expect(pkg.dependencies.react).toBeDefined() + expect(pkg.dependencies.vite).toBeDefined() + expect(pkg.devDependencies['@vitejs/plugin-react']).toBeDefined() + expect(pkg.scripts['dev:client']).toBe('vite') + + // Middleware registers inertia() after resora(). + const mw = await readFile(join(project, 'src/config/middleware.ts'), 'utf-8') + expect(mw).toContain('import { formdata, inertia, requestLogger, resora }') + expect(mw).toMatch(/resora\(\),\s*\n\s*inertia\(\),/) + + // Root route renders an Inertia page. + const web = await readFile(join(project, 'src/routes/web.ts'), 'utf-8') + expect(web).toContain("import { inertia } from '@arkstack/inertia'") + expect(web).toContain("inertia('Index'") + expect(web).not.toContain("view('welcome'") + + // tsconfig enables JSX and includes resources/. + const ts = JSON.parse(await readFile(join(project, 'tsconfig.json'), 'utf-8')) + expect(ts.compilerOptions.jsx).toBe('react-jsx') + expect(ts.include).toContain('resources') + }) + + test('vue stack does not enable jsx or the react preamble', async () => { + const { repo, project } = await scaffold() + + // Provide the vue stubs the spec references. + const stubs = join(repo, 'packages/inertia/stubs') + await mkdir(join(stubs, 'vue/resources/js/Pages'), { recursive: true }) + await writeFile(join(stubs, 'vue/resources/js/app.ts.stub'), 'createInertiaApp({})\n') + await writeFile(join(stubs, 'vue/vite.config.ts.stub'), 'export default {}\n') + await writeFile(join(stubs, 'vue/resources/js/Pages/Index.vue.stub'), '\n') + + const actions = new Actions(project) + await actions.captureInertiaStubs(repo) + await actions.applyInertia('express', 'vue') + + expect(existsSync(join(project, 'resources/js/app.ts'))).toBe(true) + expect(existsSync(join(project, 'resources/js/Pages/Index.vue'))).toBe(true) + + const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') + expect(edge).not.toContain('@viteReactRefresh') + expect(edge).toContain('resources/js/app.ts') + + const pkg = JSON.parse(await readFile(join(project, 'package.json'), 'utf-8')) + expect(pkg.dependencies['@inertiajs/vue3']).toBeDefined() + expect(pkg.devDependencies['@vitejs/plugin-vue']).toBeDefined() + + const ts = JSON.parse(await readFile(join(project, 'tsconfig.json'), 'utf-8')) + expect(ts.compilerOptions.jsx).toBeUndefined() + expect(ts.include).toContain('resources') + }) + + test('adds the React preamble even when the stub predates the {{reactRefresh}} placeholder', async () => { + const { repo, project } = await scaffold() + + // Simulate an older downloaded stub: no placeholder, hardcoded `app.ts`. + const stubs = join(repo, 'packages/inertia/stubs') + await writeFile(join(stubs, 'views/app.edge.stub'), "@inertiaHead\n @vite('resources/js/app.ts')\n") + + const actions = new Actions(project) + await actions.captureInertiaStubs(repo) + await actions.applyInertia('express', 'react') + + const edge = await readFile(join(project, 'src/resources/views/app.edge'), 'utf-8') + expect(edge).toContain('@viteReactRefresh') + // The preamble must sit immediately before the @vite tag. + expect(edge).toMatch(/@viteReactRefresh\n\s*@vite\(/) + // The entry extension is corrected to React's `tsx`. + expect(edge).toContain('resources/js/app.tsx') + expect(edge).not.toContain("resources/js/app.ts'") + }) + + test('skips gracefully when stubs were never captured', async () => { + const project = await mkdtemp(join(tmpdir(), 'create-arkstack-proj-')) + tempDirs.push(project) + await writeFile(join(project, 'package.json'), JSON.stringify({ dependencies: {} })) + + const actions = new Actions(project) + // No captureInertiaStubs() call → nothing to apply. + await actions.applyInertia('express', 'react') + + expect(existsSync(join(project, 'resources/js/app.tsx'))).toBe(false) + }) }) diff --git a/create-arkstack/tests/lean-profile.test.ts b/create-arkstack/tests/lean-profile.test.ts index 84b48afe..02584f4d 100644 --- a/create-arkstack/tests/lean-profile.test.ts +++ b/create-arkstack/tests/lean-profile.test.ts @@ -9,101 +9,109 @@ import { tmpdir } from 'node:os' const tempDirs: string[] = [] afterEach(async () => { - await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) - tempDirs.length = 0 + await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true }))) + tempDirs.length = 0 }) describe('makeLeanProfile', () => { - test('removes controllers,resources dirs, api route, and prisma/database artifacts from lean kits', async () => { - const location = await mkdtemp(join(tmpdir(), 'create-arkstack-lean-')) - tempDirs.push(location) + test('removes controllers,resources dirs, api route, and prisma/database artifacts from lean kits', async () => { + const location = await mkdtemp(join(tmpdir(), 'create-arkstack-lean-')) + tempDirs.push(location) - await mkdir(join(location, 'src/database/migrations'), { recursive: true }) - await mkdir(join(location, 'src/app/http/controllers'), { recursive: true }) - await mkdir(join(location, 'src/app/http/resources'), { recursive: true }) - await mkdir(join(location, 'src/app/models'), { recursive: true }) - await mkdir(join(location, 'src/core'), { recursive: true }) - await mkdir(join(location, 'src/routes'), { recursive: true }) - await mkdir(join(location, 'prisma/migrations'), { recursive: true }) + await mkdir(join(location, 'src/database/migrations'), { recursive: true }) + await mkdir(join(location, 'src/app/http/controllers'), { + recursive: true, + }) + await mkdir(join(location, 'src/app/http/resources'), { recursive: true }) + await mkdir(join(location, 'src/app/models'), { recursive: true }) + await mkdir(join(location, 'src/core'), { recursive: true }) + await mkdir(join(location, 'src/routes'), { recursive: true }) + await mkdir(join(location, 'prisma/migrations'), { recursive: true }) - await writeFile(join(location, 'src/app/http/controllers/UserController.ts'), 'export default class UserController {}\n') - await writeFile(join(location, 'src/app/http/resources/UserCollection.ts'), 'export default class UserCollection {}\n') - await writeFile(join(location, 'src/app/http/resources/UserResource.ts'), 'export default class UserResource {}\n') - await writeFile(join(location, 'src/app/models/UserModel.ts'), 'export default class UserModel {}\n') - await writeFile(join(location, 'src/database/migrations/0000_initial.ts'), 'export default class InitialMigration {}\n') - await writeFile(join(location, 'src/core/database.ts'), 'export const prisma = {} as any;\n') - await writeFile(join(location, 'src/core/bootstrap.ts'), 'import \'@arkstack/database/setup\'') - await writeFile(join(location, 'prisma.config.ts'), 'export default {};\n') - await writeFile(join(location, 'arkormx.config.ts'), 'export default {};\n') - await writeFile(join(location, 'prisma/migrations/migration_lock.toml'), '# lock\n') + await writeFile( + join(location, 'src/app/http/controllers/UserController.ts'), + 'export default class UserController {}\n', + ) + await writeFile( + join(location, 'src/app/http/resources/UserCollection.ts'), + 'export default class UserCollection {}\n', + ) + await writeFile(join(location, 'src/app/http/resources/UserResource.ts'), 'export default class UserResource {}\n') + await writeFile(join(location, 'src/app/models/UserModel.ts'), 'export default class UserModel {}\n') + await writeFile( + join(location, 'src/database/migrations/0000_initial.ts'), + 'export default class InitialMigration {}\n', + ) + await writeFile(join(location, 'src/core/database.ts'), 'export const prisma = {} as any;\n') + await writeFile(join(location, 'src/core/bootstrap.ts'), "import '@arkstack/database/setup'") + await writeFile(join(location, 'prisma.config.ts'), 'export default {};\n') + await writeFile(join(location, 'arkormx.config.ts'), 'export default {};\n') + await writeFile(join(location, 'prisma/migrations/migration_lock.toml'), '# lock\n') - await writeFile( - join(location, 'src/core/app.ts'), - [ - 'export default class Application {', - ' /**', - ' * Shuts down the application by disconnecting from the database and exiting the process.', - ' */', - ' async shutdown () {', - ' process.exit(0)', - ' }', - '}', - '', - ].join('\n'), - ) + await writeFile( + join(location, 'src/core/app.ts'), + [ + 'export default class Application {', + ' /**', + ' * Shuts down the application by disconnecting from the database and exiting the process.', + ' */', + ' async shutdown () {', + ' process.exit(0)', + ' }', + '}', + '', + ].join('\n'), + ) - await writeFile( - join(location, 'src/routes/api.ts'), - 'Router.get(\'/stale\', () => [])\n', - ) + await writeFile(join(location, 'src/routes/api.ts'), "Router.get('/stale', () => [])\n") - await writeFile( - join(location, 'package.json'), - JSON.stringify( - { - dependencies: { - pg: '^8.18.0', - keep: '^1.0.0', - kysely: '^0.28.15', - arkormx: '^0.2.0', - }, - devDependencies: { - prisma: '^7.4.0', - '@types/pg': '^8.16.0', - keepDev: '^1.0.0', - }, - }, - null, - 2, - ), - ) + await writeFile( + join(location, 'package.json'), + JSON.stringify( + { + dependencies: { + pg: '^8.18.0', + keep: '^1.0.0', + kysely: '^0.28.15', + arkormx: '^0.2.0', + }, + devDependencies: { + prisma: '^7.4.0', + '@types/pg': '^8.16.0', + keepDev: '^1.0.0', + }, + }, + null, + 2, + ), + ) - const actions = new Actions(location) - await actions.makeLeanProfile('express') - await actions.saveProfile() + const actions = new Actions(location) + await actions.makeLeanProfile('express') + await actions.saveProfile() - expect(existsSync(join(location, 'src/app/http/controllers'))).toBe(false) - expect(existsSync(join(location, 'src/app/http/resources'))).toBe(false) - expect(existsSync(join(location, 'src/app/models'))).toBe(false) - expect(existsSync(join(location, 'src/routes/api.ts'))).toBe(false) - expect(existsSync(join(location, 'src/database'))).toBe(false) - expect(existsSync(join(location, 'src/core/database.ts'))).toBe(false) - expect(existsSync(join(location, 'prisma.config.ts'))).toBe(false) - expect(existsSync(join(location, 'arkormx.config.ts'))).toBe(false) - expect(existsSync(join(location, 'prisma'))).toBe(false) + expect(existsSync(join(location, 'src/app/http/controllers'))).toBe(false) + expect(existsSync(join(location, 'src/app/http/resources'))).toBe(false) + expect(existsSync(join(location, 'src/app/models'))).toBe(false) + expect(existsSync(join(location, 'src/routes/api.ts'))).toBe(false) + expect(existsSync(join(location, 'src/database'))).toBe(false) + expect(existsSync(join(location, 'src/core/database.ts'))).toBe(false) + expect(existsSync(join(location, 'prisma.config.ts'))).toBe(false) + expect(existsSync(join(location, 'arkormx.config.ts'))).toBe(false) + expect(existsSync(join(location, 'prisma'))).toBe(false) - const pkg = JSON.parse(await readFile(join(location, 'package.json'), 'utf-8')) - expect(pkg.dependencies.kysely).toBeUndefined() - expect(pkg.dependencies.arkormx).toBeUndefined() - expect(pkg.dependencies.pg).toBeUndefined() - expect(pkg.devDependencies.prisma).toBeUndefined() - expect(pkg.dependencies.keep).toBe('^1.0.0') - expect(pkg.devDependencies.keepDev).toBe('^1.0.0') + const pkg = JSON.parse(await readFile(join(location, 'package.json'), 'utf-8')) + expect(pkg.dependencies.kysely).toBeUndefined() + expect(pkg.dependencies.arkormx).toBeUndefined() + expect(pkg.dependencies.pg).toBeUndefined() + expect(pkg.devDependencies.prisma).toBeUndefined() + expect(pkg.dependencies.keep).toBe('^1.0.0') + expect(pkg.devDependencies.keepDev).toBe('^1.0.0') - const appContent = await readFile(join(location, 'src/core/app.ts'), 'utf-8') - expect(appContent).not.toContain('import { ModelNotFoundException } from \'arkormx\'') + const appContent = await readFile(join(location, 'src/core/app.ts'), 'utf-8') + expect(appContent).not.toContain("import { ModelNotFoundException } from 'arkormx'") - const bootContent = await readFile(join(location, 'src/core/bootstrap.ts'), 'utf-8') - expect(bootContent).not.toContain('import \'@arkstack/database/setup\'') - }) + const bootContent = await readFile(join(location, 'src/core/bootstrap.ts'), 'utf-8') + expect(bootContent).not.toContain("import '@arkstack/database/setup'") + }) }) diff --git a/create-arkstack/tsdown.config.ts b/create-arkstack/tsdown.config.ts index a11753af..35c24bb4 100644 --- a/create-arkstack/tsdown.config.ts +++ b/create-arkstack/tsdown.config.ts @@ -1,26 +1,21 @@ import { defineConfig } from 'tsdown' export default defineConfig({ - entry: ['src/run.ts'], - format: 'esm', - outDir: 'bin', - dts: false, - minify: true, - exports: true, - sourcemap: false, - deps: { - neverBundle: [ - 'fs', - 'path', - 'os', - 'dotenv' - ] - }, - clean: true, - outExtensions: () => { - return ({ - js: '.js', - dts: '.d.ts' - }) - }, -}) + entry: ['src/run.ts'], + format: 'esm', + outDir: 'bin', + dts: false, + minify: true, + exports: true, + sourcemap: false, + deps: { + neverBundle: ['fs', 'path', 'os', 'dotenv'], + }, + clean: true, + outExtensions: () => { + return { + js: '.js', + dts: '.d.ts', + } + }, +}) diff --git a/database/migrations/20260428165211_create_users_table.ts b/database/migrations/20260428165211_create_users_table.ts index e1ba062d..ac682d6b 100644 --- a/database/migrations/20260428165211_create_users_table.ts +++ b/database/migrations/20260428165211_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() - table.string('email').unique().index() - table.string('password') - table.string('name') - table.timestamps() - }) - } + public async up(schema: SchemaBuilder): Promise { + schema.createTable("users", (table) => { + table.id(); + table.string("email").unique().index(); + table.string("password"); + table.string("name"); + 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/database/migrations/20260428165230_create_person_access_tokens_table.ts b/database/migrations/20260428165230_create_person_access_tokens_table.ts index b4cb5c54..ae577e18 100644 --- a/database/migrations/20260428165230_create_person_access_tokens_table.ts +++ b/database/migrations/20260428165230_create_person_access_tokens_table.ts @@ -1,22 +1,28 @@ -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() - table.bigInteger('userId').map('user_id') - .foreign().references('users', 'id').onDelete('cascade').as('user').inverseAlias('personalAccessTokens') - table.string('name') - table.text('token').unique() - table.json('abilities') - 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(); + table + .bigInteger("userId") + .map("user_id") + .foreign() + .references("users", "id") + .onDelete("cascade") + .as("user") + .inverseAlias("personalAccessTokens"); + table.string("name"); + table.text("token").unique(); + table.json("abilities"); + 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') - } -} \ No newline at end of file + public async down(schema: SchemaBuilder): Promise { + schema.dropTable("personal_access_tokens"); + } +} diff --git a/database/migrations/20260505110000_create_user_two_factors_table.ts b/database/migrations/20260505110000_create_user_two_factors_table.ts index 078d0594..0dd5d80c 100644 --- a/database/migrations/20260505110000_create_user_two_factors_table.ts +++ b/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() - table.bigInteger('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(); + table + .bigInteger("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/database/migrations/20260505111000_create_user_notifications_table.ts b/database/migrations/20260505111000_create_user_notifications_table.ts index c813b706..27f1afeb 100644 --- a/database/migrations/20260505111000_create_user_notifications_table.ts +++ b/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() - table.bigInteger('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(); + table + .bigInteger("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/database/migrations/20260524000000_create_sessions_table.ts b/database/migrations/20260524000000_create_sessions_table.ts index d61c2d64..e3f71385 100644 --- a/database/migrations/20260524000000_create_sessions_table.ts +++ b/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/database/models/PersonalAccessToken.ts b/database/models/PersonalAccessToken.ts index 051d4263..81e21397 100644 --- a/database/models/PersonalAccessToken.ts +++ b/database/models/PersonalAccessToken.ts @@ -2,15 +2,15 @@ import { PersonalAccessToken as BasePersonalAccessToken } from '../../packages/a import User from './User' export default class PersonalAccessToken extends BasePersonalAccessToken { - protected static table = 'personal_access_tokens' - protected static columns = { - userId: 'user_id', - deviceInfo: 'device_info', - lastUsedAt: 'last_used_at', - expiresAt: 'expires_at', - } + protected static table = 'personal_access_tokens' + protected static columns = { + userId: 'user_id', + deviceInfo: 'device_info', + lastUsedAt: 'last_used_at', + expiresAt: 'expires_at', + } - user () { - return this.belongsTo(User, 'userId') - } + user() { + return this.belongsTo(User, 'userId') + } } diff --git a/database/models/User.ts b/database/models/User.ts index 5d9a7a2e..6b4a6f72 100644 --- a/database/models/User.ts +++ b/database/models/User.ts @@ -4,17 +4,17 @@ import UserNotification from './UserNotification' import UserTwoFactor from './UserTwoFactor' export default class User extends BaseUser { - protected static table = 'users' + protected static table = 'users' - 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/database/models/UserNotification.ts b/database/models/UserNotification.ts index 4df10572..31917399 100644 --- a/database/models/UserNotification.ts +++ b/database/models/UserNotification.ts @@ -3,17 +3,17 @@ import { UserNotification as BaseUserNotification } from '../../packages/notific import User from './User' export default class UserNotification extends BaseUserNotification { - protected static table = 'user_notifications' - protected static columns = { - userId: 'user_id', - actionText: 'action_text', - actionLink: 'action_link', - readAt: 'read_at', - createdAt: 'created_at', - updatedAt: 'updated_at', - } + protected static table = 'user_notifications' + 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/database/models/UserTwoFactor.ts b/database/models/UserTwoFactor.ts index 1d3ac919..4c49436c 100644 --- a/database/models/UserTwoFactor.ts +++ b/database/models/UserTwoFactor.ts @@ -3,18 +3,18 @@ import User from './User' import { BelongsToRelation } from 'arkormx/relationship' export default class UserTwoFactor extends BaseUserTwoFactor { - protected static override table = 'user_two_factors' - protected static override 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', - } + protected static override table = 'user_two_factors' + protected static override 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', + } - user (): BelongsToRelation { - return this.belongsTo(User, 'userId', 'id') as never - } + user(): BelongsToRelation { + return this.belongsTo(User, 'userId', 'id') as never + } } diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4351111e..a1815f0b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -4,22 +4,46 @@ import { defineConfig } from 'vitepress' export default defineConfig({ cleanUrls: true, // base: '/arkstack/', - title: "Arkstack", - description: "Runtime-agnostic TypeScript backend framework for structured server applications", + title: 'Arkstack', + description: 'Runtime-agnostic TypeScript backend framework for structured server applications', head: [ ['link', { rel: 'icon', href: '/logo.png' }], ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }], - ['meta', { name: 'description', content: 'Runtime-agnostic TypeScript backend framework for structured server applications' }], - ['meta', { name: 'keywords', content: 'API, Web, Node.js, TypeScript, Backend Framework, Runtime Agnostic' }], + [ + 'meta', + { + name: 'description', + content: 'Runtime-agnostic TypeScript backend framework for structured server applications', + }, + ], + [ + 'meta', + { + name: 'keywords', + content: 'API, Web, Node.js, TypeScript, Backend Framework, Runtime Agnostic', + }, + ], ['meta', { name: 'author', content: 'Toneflix' }], ['meta', { property: 'og:title', content: 'Arkstack' }], - ['meta', { property: 'og:description', content: 'Runtime-agnostic TypeScript backend framework for structured server applications' }], + [ + 'meta', + { + property: 'og:description', + content: 'Runtime-agnostic TypeScript backend framework for structured server applications', + }, + ], ['meta', { property: 'og:image', content: '/banner.jpg' }], ['meta', { property: 'og:url', content: 'https://arkstack.toneflix.net' }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:title', content: 'Arkstack' }], - ['meta', { name: 'twitter:description', content: 'Runtime-agnostic TypeScript backend framework for structured server applications' }], - ['meta', { name: 'twitter:image', content: '/banner.jpg' }] + [ + 'meta', + { + name: 'twitter:description', + content: 'Runtime-agnostic TypeScript backend framework for structured server applications', + }, + ], + ['meta', { name: 'twitter:image', content: '/banner.jpg' }], ], themeConfig: { logo: '/logo.png', @@ -28,7 +52,7 @@ export default defineConfig({ { text: 'Home', link: '/' }, { text: 'Guide', link: '/guide/getting-started' }, { text: 'Architecture', link: '/architecture/overview' }, - { text: 'API', link: '/api' } + { text: 'API', link: '/api' }, ], sidebar: [ @@ -43,9 +67,12 @@ export default defineConfig({ { text: 'Helpers', link: '/guide/utilities/helpers' }, { text: 'Hashing', link: '/guide/utilities/hashing' }, { text: 'Encryption', link: '/guide/utilities/encryption' }, - { text: 'End-to-End Encryption', link: '/guide/utilities/e2e-encryption' }, + { + text: 'End-to-End Encryption', + link: '/guide/utilities/e2e-encryption', + }, { text: 'Trait System', link: '/guide/utilities/trait-system' }, - ] + ], }, { text: 'Responses & Resources', @@ -53,10 +80,22 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Introduction', link: '/guide/responses' }, - { text: 'Resources', link: 'https://arkstack-hq.github.io/resora/guide/resources' }, - { text: 'Collections', link: 'https://arkstack-hq.github.io/resora/guide/collections' }, - { text: 'Generating Resources', link: 'https://arkstack-hq.github.io/resora/guide/generating-resources' }, - { text: 'Resora Reference', link: 'https://arkstack-hq.github.io/resora/guide/getting-started' }, + { + text: 'Resources', + link: 'https://arkstack-hq.github.io/resora/guide/resources', + }, + { + text: 'Collections', + link: 'https://arkstack-hq.github.io/resora/guide/collections', + }, + { + text: 'Generating Resources', + link: 'https://arkstack-hq.github.io/resora/guide/generating-resources', + }, + { + text: 'Resora Reference', + link: 'https://arkstack-hq.github.io/resora/guide/getting-started', + }, ], }, { @@ -65,17 +104,50 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Introduction', link: '/guide/database-modeling' }, - { text: 'Models', link: 'https://arkormx.toneflix.net/guide/models' }, - { text: 'Mutators & Accessors', link: 'https://arkormx.toneflix.net/guide/mutators' }, - { text: 'Casting', link: 'https://arkormx.toneflix.net/guide/casting' }, - { text: 'Query Builder', link: 'https://arkormx.toneflix.net/guide/query-builder' }, - { text: 'Expressions & Aggregation', link: 'https://arkormx.toneflix.net/guide/expressions' }, - { text: 'Collections', link: 'https://arkormx.toneflix.net/guide/collections' }, - { text: 'Transactions', link: 'https://arkormx.toneflix.net/guide/transactions' }, - { text: 'Pagination', link: 'https://arkormx.toneflix.net/guide/pagination' }, - { text: 'Relationships', link: 'https://arkormx.toneflix.net/guide/relationships' }, - { text: 'Factories and Seeders', link: 'https://arkormx.toneflix.net/guide/factories-seeders' }, - { text: 'Arkormˣ Reference', link: 'https://arkormx.toneflix.net/guide/getting-started' }, + { + text: 'Models', + link: 'https://arkormx.toneflix.net/guide/models', + }, + { + text: 'Mutators & Accessors', + link: 'https://arkormx.toneflix.net/guide/mutators', + }, + { + text: 'Casting', + link: 'https://arkormx.toneflix.net/guide/casting', + }, + { + text: 'Query Builder', + link: 'https://arkormx.toneflix.net/guide/query-builder', + }, + { + text: 'Expressions & Aggregation', + link: 'https://arkormx.toneflix.net/guide/expressions', + }, + { + text: 'Collections', + link: 'https://arkormx.toneflix.net/guide/collections', + }, + { + text: 'Transactions', + link: 'https://arkormx.toneflix.net/guide/transactions', + }, + { + text: 'Pagination', + link: 'https://arkormx.toneflix.net/guide/pagination', + }, + { + text: 'Relationships', + link: 'https://arkormx.toneflix.net/guide/relationships', + }, + { + text: 'Factories and Seeders', + link: 'https://arkormx.toneflix.net/guide/factories-seeders', + }, + { + text: 'Arkormˣ Reference', + link: 'https://arkormx.toneflix.net/guide/getting-started', + }, ], }, { @@ -88,7 +160,10 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Introduction', link: '/guide/validation' }, - { text: 'Kanun Reference', link: 'https://arkstack-hq.github.io/kanun/guide/getting-started' }, + { + text: 'Kanun Reference', + link: 'https://arkstack-hq.github.io/kanun/guide/getting-started', + }, ], }, { @@ -97,7 +172,10 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Introduction', link: '/guide/http-testing' }, - { text: 'Parasito Reference', link: 'https://github.com/arkstack-hq/parasito#readme' }, + { + text: 'Parasito Reference', + link: 'https://github.com/arkstack-hq/parasito#readme', + }, ], }, ], @@ -116,21 +194,24 @@ export default defineConfig({ { text: 'HTTP', link: '/guide/http' }, { text: 'Deployment', link: '/guide/deployment' }, { text: 'API Reference', link: '/api' }, - ] + ], }, { text: 'Runtime Interaction', items: [ { text: 'Express', link: '/guide/express-runtime' }, { text: 'H3', link: '/guide/h3-runtime' }, - ] + ], }, { text: 'Architecture', items: [ { text: 'Overview', link: '/architecture/overview' }, - { text: 'Unification Roadmap', link: '/architecture/unification-roadmap' }, - ] + { + text: 'Unification Roadmap', + link: '/architecture/unification-roadmap', + }, + ], }, { text: 'More', @@ -139,8 +220,8 @@ export default defineConfig({ { text: 'Roadmap', link: '/more/roadmap' }, { text: 'Contributing', link: '/more/contributing' }, { text: 'Changelog', link: '/more/changelog' }, - ] - } + ], + }, ], socialLinks: [ @@ -149,6 +230,6 @@ export default defineConfig({ { icon: 'npm', link: 'https://www.npmjs.com/package/create-arkstack' }, ], - footer: {} - } + footer: {}, + }, }) diff --git a/docs/.vitepress/data.ts b/docs/.vitepress/data.ts index 95558cc2..99bb8ef2 100644 --- a/docs/.vitepress/data.ts +++ b/docs/.vitepress/data.ts @@ -6,15 +6,12 @@ export const truncate = (text: string, length = 120, suffix = '...') => { const truncated = text.slice(0, length - suffix.length) const lastSpace = truncated.lastIndexOf(' ') - const result = lastSpace <= 0 - ? truncated - : truncated.slice(0, lastSpace) + const result = lastSpace <= 0 ? truncated : truncated.slice(0, lastSpace) - return (result.trimEnd().replace(/[.,;:!?]+$/, '') + suffix) + return result.trimEnd().replace(/[.,;:!?]+$/, '') + suffix } export const fitText = (text: string) => { - const parse1 = text.split('package for') const parse2 = (parse1.at(1) ?? parse1.at(0))?.split('providing') ?? [] const fixed = parse2.at(1) ?? parse2.at(0)?.trim() ?? '' @@ -49,31 +46,29 @@ export const abbreviateNumber = (value: string | number) => { return `${Math.floor(number / 1_000_000_000)}b` } -export async function getTotalDownloads (packageName: string, fromYear = 2015) { +export async function getTotalDownloads(packageName: string, fromYear = 2015) { const currentYear = new Date().getFullYear() let total = 0 try { for (let year = fromYear; year <= currentYear; year++) { const from = `${year}-01-01` - const to = year === currentYear - ? new Date().toISOString().slice(0, 10) - : `${year}-12-31` + const to = year === currentYear ? new Date().toISOString().slice(0, 10) : `${year}-12-31` const encoded = encodeURIComponent(packageName) const url = `https://api.npmjs.org/downloads/range/${from}:${to}/${encoded}` - const response = await fetch(url).catch(e => e) + const response = await fetch(url).catch((e) => e) const data = await response.json() - total += data.downloads?.reduce((sum: number, item: any) => { - return sum + item.downloads - }, 0) ?? 0 + total += + data.downloads?.reduce((sum: number, item: any) => { + return sum + item.downloads + }, 0) ?? 0 } - } catch { // } return total -} \ No newline at end of file +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 9b3774c3..53383c8d 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -13,8 +13,8 @@ export default { // https://vitepress.dev/guide/extending-default-theme#layout-slots }) }, - enhanceApp ({ app, router, siteData }) { + enhanceApp({ app, router, siteData }) { app.component('icon', HIcon) app.component('landing', LandingLayout) - } + }, } satisfies Theme diff --git a/docs/api.md b/docs/api.md index 793dba7b..ddbd68a7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -202,13 +202,13 @@ Driver packages: Express: ```ts -import { auth } from '@arkstack/driver-express/middlewares'; +import { auth } from '@arkstack/driver-express/middlewares' ``` H3: ```ts -import { auth } from '@arkstack/driver-h3/middlewares'; +import { auth } from '@arkstack/driver-h3/middlewares' ``` Both middlewares expect an `Authorization: Bearer ` header. @@ -365,13 +365,13 @@ The `pusher-js` / `firebase` SDKs (and `react` / `vue` for the bindings) are opt Express: ```ts -import { inertia } from '@arkstack/driver-express/middlewares'; +import { inertia } from '@arkstack/driver-express/middlewares' ``` H3: ```ts -import { inertia } from '@arkstack/driver-h3/middlewares'; +import { inertia } from '@arkstack/driver-h3/middlewares' ``` Register it in `src/config/middleware.ts` under `before` (alongside `resora()`). The middleware binds the Inertia request context and upgrades mutation redirects to `303`. @@ -420,14 +420,14 @@ Decorating a controller method with `@Bind()` enables typed injection of `Reques Uncaught exceptions are serialized to a standard payload — `{ status, code, message, errors?, stack? }`. To add fields to, or reshape, that response, set a `body` on an `AppException` (or a subclass). When present, `body` is merged **over** the standard payload, so it can both add custom fields and override the defaults (`status`, `code`, `message`, …). ```ts -import { AppException } from '@arkstack/common'; +import { AppException } from '@arkstack/common' // Add fields to the standard error payload class PaymentException extends AppException { - body = { error_code: 'PAYMENT_FAILED', retryable: true }; + body = { error_code: 'PAYMENT_FAILED', retryable: true } } -throw new PaymentException('Payment failed', 402); +throw new PaymentException('Payment failed', 402) // → { status: 'error', code: 402, message: 'Payment failed', // error_code: 'PAYMENT_FAILED', retryable: true } ``` diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index b414ae08..c12b5934 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -159,7 +159,7 @@ Hook.set('middleware:auth', { before: ({ req, res }) => {}, after: ({ req, res }) => {}, error: (error, { req, res }) => {}, -}); +}) ``` ### View Composers @@ -168,8 +168,8 @@ View composers attach data before rendering: ```ts View.composer('dashboard', (view) => { - view.with('title', 'Dashboard'); -}); + view.with('title', 'Dashboard') +}) ``` Composers can be functions, classes, or instances with a `compose()` method. @@ -179,8 +179,8 @@ Composers can be functions, classes, or instances with a `compose()` method. Packages can ship their own `resources/views` directory and expose templates through tilde names: ```ts -await view('~package-name.mail'); -await view('~org/package-name.mail'); +await view('~package-name.mail') +await view('~org/package-name.mail') ``` ### Console Commands diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 0d97049a..fecf58ba 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -47,41 +47,41 @@ By default, Arkstack looks in `src/app/models`. ```ts // src/app/models/User.ts -import { User as BaseUser } from '@arkstack/auth'; -import PersonalAccessToken from './PersonalAccessToken'; +import { User as BaseUser } from '@arkstack/auth' +import PersonalAccessToken from './PersonalAccessToken' export default 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 columns = { createdAt: 'created_at', updatedAt: 'updated_at', - }; + } personalAccessTokens() { - return this.hasMany(PersonalAccessToken, 'userId'); + return this.hasMany(PersonalAccessToken, 'userId') } } ``` ```ts // src/app/models/PersonalAccessToken.ts -import { PersonalAccessToken as BasePersonalAccessToken } from '@arkstack/auth'; -import User from './User'; +import { PersonalAccessToken as BasePersonalAccessToken } from '@arkstack/auth' +import User from './User' export default 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', @@ -90,10 +90,10 @@ export default class PersonalAccessToken extends BasePersonalAccessToken { expiresAt: 'expires_at', createdAt: 'created_at', updatedAt: 'updated_at', - }; + } user() { - return this.belongsTo(User, 'userId'); + return this.belongsTo(User, 'userId') } } ``` @@ -103,19 +103,19 @@ export default class PersonalAccessToken extends BasePersonalAccessToken { Use `Auth` inside a Clear Router route to verify credentials and create a personal access token. ```ts -import { Auth } from '@arkstack/auth'; -import { Router } from '@arkstack/driver-express'; +import { Auth } from '@arkstack/auth' +import { Router } from '@arkstack/driver-express' Router.post('/auth/login', async ({ req, res }) => { - const { email, password } = req.body; - const auth = Auth.make().setRequest(req); - const personalAccessToken = await auth.login(email, password); + const { email, password } = req.body + const auth = Auth.make().setRequest(req) + const personalAccessToken = await auth.login(email, password) return res.status(200).json({ token: personalAccessToken.token, user: personalAccessToken.user, - }); -}); + }) +}) ``` `Auth.login()` validates the user password with Arkstack's hash utility, creates a JWT with `jose`, stores it as a personal access token, and associates the token with the current device when request information is available. @@ -125,18 +125,18 @@ Router.post('/auth/login', async ({ req, res }) => { Clear Router accepts middleware as the final argument on a route. Use the auth middleware from your active driver package and attach it to any protected route. ```ts -import { auth } from '@arkstack/driver-express/middlewares'; -import { Router } from '@arkstack/driver-express'; +import { auth } from '@arkstack/driver-express/middlewares' +import { Router } from '@arkstack/driver-express' Router.get( '/account', ({ req, res }) => { return res.status(200).json({ user: req.authUser, - }); + }) }, [auth], -); +) ``` For other runtimes, import `auth` from `@arkstack/driver-[framework]/middlewares`. The middleware placement in the Clear Router route stays the same. @@ -154,21 +154,21 @@ The auth middleware exposes a runtime-agnostic `middleware:auth` hook through `H The hook contract is the same across runtime drivers: middleware context is passed as `{ req, res }`, and error hooks receive the error first. ```ts -import { Hook } from '@arkstack/foundry'; +import { Hook } from '@arkstack/foundry' Hook.set('middleware:auth', { before: ({ req, res }) => { - console.log('auth middleware starting'); + console.log('auth middleware starting') }, after: ({ req, res }) => { console.log('auth middleware passed', { userId: req.authUser?.id, - }); + }) }, error: (error, { req, res }) => { - console.error('auth middleware failed', { error }); + console.error('auth middleware failed', { error }) }, -}); +}) ``` Hook positions receive: @@ -184,9 +184,9 @@ Calling `Hook.set('middleware:auth', ...)` again merges the supplied positions w Use a Clear Router group when several routes share the same auth gate: ```ts -import { auth } from '@arkstack/driver-express/middlewares'; -import { Auth } from '@arkstack/auth'; -import { Router } from '@arkstack/driver-express'; +import { auth } from '@arkstack/driver-express/middlewares' +import { Auth } from '@arkstack/auth' +import { Router } from '@arkstack/driver-express' await Router.group( '/account', @@ -194,19 +194,19 @@ await Router.group( Router.get('/profile', ({ req, res }) => { return res.status(200).json({ user: req.authUser, - }); - }); + }) + }) Router.get('/sessions', async ({ req, res }) => { - const session = await Auth.make().setRequest(req).session().token(); + const session = await Auth.make().setRequest(req).session().token() return res.status(200).json({ session, - }); - }); + }) + }) }, [auth], -); +) ``` ## Authenticated Context @@ -225,21 +225,21 @@ For most application code, prefer using those values from the Clear Router handl Use `session()` when you need the database record for the current bearer token: ```ts -import { Auth } from '@arkstack/auth'; -import { auth } from '@arkstack/driver-express/middlewares'; -import { Router } from '@arkstack/driver-express'; +import { Auth } from '@arkstack/auth' +import { auth } from '@arkstack/driver-express/middlewares' +import { Router } from '@arkstack/driver-express' Router.get( '/account/session', async ({ req, res }) => { - const session = await Auth.make().setRequest(req).session().token(); + const session = await Auth.make().setRequest(req).session().token() return res.status(200).json({ session, - }); + }) }, [auth], -); +) ``` This is useful for showing device/session metadata or revoking the current token. @@ -249,11 +249,8 @@ This is useful for showing device/session metadata or revoking the current token Temporary tokens are useful for flows like two-factor authentication: ```ts -const token = await Auth.make().createTemporaryToken(user, 'two-factor', '10m'); -const verifiedUser = await Auth.make().authorizeTemporaryToken( - token, - 'two-factor', -); +const token = await Auth.make().createTemporaryToken(user, 'two-factor', '10m') +const verifiedUser = await Auth.make().authorizeTemporaryToken(token, 'two-factor') ``` Temporary tokens are JWTs and are not stored as personal access tokens. @@ -281,21 +278,18 @@ TWO_FACTOR_SMS_TTL_MINUTES="10" Create a setup secret, show the returned `otpauthUrl` as a QR code, then persist the secret after the user confirms a valid code. ```ts -import { TwoFactor } from '@arkstack/auth'; +import { TwoFactor } from '@arkstack/auth' -const setup = TwoFactor.createSetup(user); +const setup = TwoFactor.createSetup(user) -await TwoFactor.setSecret(user.id, setup.secret); +await TwoFactor.setSecret(user.id, setup.secret) if (TwoFactor.verifyCode(user, setup.secret, code)) { - const recoveryCodes = TwoFactor.generateBackupCodes(); - - await TwoFactor.setMethod(user.id, 'authenticator'); - await TwoFactor.setEnabledAt(user.id); - await TwoFactor.writeRecoveryCodeHashes( - user.id, - await TwoFactor.hashBackupCodes(recoveryCodes), - ); + const recoveryCodes = TwoFactor.generateBackupCodes() + + await TwoFactor.setMethod(user.id, 'authenticator') + await TwoFactor.setEnabledAt(user.id) + await TwoFactor.writeRecoveryCodeHashes(user.id, await TwoFactor.hashBackupCodes(recoveryCodes)) } ``` @@ -304,23 +298,21 @@ if (TwoFactor.verifyCode(user, setup.secret, code)) { `TwoFactor.issueSmsCode()` creates and stores a hashed challenge. Deliver the returned code with `@arkstack/notifications`. ```ts -import { TwoFactor } from '@arkstack/auth'; -import { Notification } from '@arkstack/notifications'; +import { TwoFactor } from '@arkstack/auth' +import { Notification } from '@arkstack/notifications' -const issued = await TwoFactor.issueSmsCode(user, 'login'); +const issued = await TwoFactor.issueSmsCode(user, 'login') -await Notification.sms() - .recipient(user.phone) - .send('Your {app} login code is {code}.', undefined, undefined, { - app: 'Arkstack', - code: issued.code, - }); +await Notification.sms().recipient(user.phone).send('Your {app} login code is {code}.', undefined, undefined, { + app: 'Arkstack', + code: issued.code, +}) ``` Configure the SMS transport in `src/config/notifications.ts`: ```ts -import { env } from '@arkstack/common'; +import { env } from '@arkstack/common' export default () => ({ default_driver: 'mail', @@ -342,7 +334,7 @@ export default () => ({ from: env('TWILIO_FROM'), }, }, -}); +}) ``` `drivers.sms.transport` selects the provider transport. Use `SMS_TRANSPORT=twilio` or `SMS_TRANSPORT=africastalking` in template config if you want to choose it from the environment. @@ -350,22 +342,19 @@ export default () => ({ Verify and consume the code: ```ts -const verified = await TwoFactor.verifySmsCode(user.id, code, 'login'); +const verified = await TwoFactor.verifySmsCode(user.id, code, 'login') if (!verified) { - throw new Error('Invalid or expired two-factor code'); + throw new Error('Invalid or expired two-factor code') } ``` Recovery codes can be generated for either two-factor method: ```ts -const recoveryCodes = TwoFactor.generateBackupCodes(); +const recoveryCodes = TwoFactor.generateBackupCodes() -await TwoFactor.writeRecoveryCodeHashes( - user.id, - await TwoFactor.hashBackupCodes(recoveryCodes), -); +await TwoFactor.writeRecoveryCodeHashes(user.id, await TwoFactor.hashBackupCodes(recoveryCodes)) -const consumed = await TwoFactor.consumeRecoveryCode(user.id, recoveryCode); +const consumed = await TwoFactor.consumeRecoveryCode(user.id, recoveryCode) ``` diff --git a/docs/guide/cache.md b/docs/guide/cache.md index 760f38a5..7946661a 100644 --- a/docs/guide/cache.md +++ b/docs/guide/cache.md @@ -39,10 +39,10 @@ Cache configuration lives in `src/config/cache.ts` — a default store, a global ```ts // src/config/cache.ts -import { Arkstack } from '@arkstack/contract'; -import { CacheConfig } from '@arkstack/cache'; -import { env } from '@arkstack/common'; -import path from 'node:path'; +import { Arkstack } from '@arkstack/contract' +import { CacheConfig } from '@arkstack/cache' +import { env } from '@arkstack/common' +import path from 'node:path' export default (): CacheConfig => ({ default: env('CACHE_STORE', 'memory'), @@ -60,7 +60,7 @@ export default (): CacheConfig => ({ }, database: { driver: 'database', table: 'cache' }, }, -}); +}) ``` The `database` store expects a table with `key` (string, primary), `value` (text), and `expiration` (nullable integer, epoch seconds) columns. Publish the ready-made migration with: @@ -75,12 +75,12 @@ pnpm ark migrate Reach for the static `Cache` facade, which proxies the default store: ```ts -import { Cache } from '@arkstack/cache'; +import { Cache } from '@arkstack/cache' -await Cache.put('user:1', user, 60); // ttl in seconds -await Cache.get('user:1'); -await Cache.has('user:1'); -await Cache.forget('user:1'); +await Cache.put('user:1', user, 60) // ttl in seconds +await Cache.get('user:1') +await Cache.has('user:1') +await Cache.forget('user:1') ``` The TTL accepts a number of seconds, a `Date`, or `null` (store forever). A non-positive TTL is treated as already expired. @@ -90,21 +90,21 @@ The TTL accepts a number of seconds, a `Date`, or `null` (store forever). A non- `remember` returns a cached value or computes, stores, and returns it (read-through caching): ```ts -const stats = await Cache.remember('stats', 300, async () => computeStats()); +const stats = await Cache.remember('stats', 300, async () => computeStats()) // never expires -const config = await Cache.rememberForever('config', () => loadConfig()); +const config = await Cache.rememberForever('config', () => loadConfig()) ``` ### Other helpers ```ts -await Cache.add('once', 'value', 60); // only stores if absent; returns false otherwise -await Cache.pull('flash'); // get and forget in one call -await Cache.forever('flag', true); // store with no expiration -await Cache.increment('hits'); // atomic on redis -await Cache.decrement('hits', 2); -await Cache.flush(); // clear the whole store +await Cache.add('once', 'value', 60) // only stores if absent; returns false otherwise +await Cache.pull('flash') // get and forget in one call +await Cache.forever('flag', true) // store with no expiration +await Cache.increment('hits') // atomic on redis +await Cache.decrement('hits', 2) +await Cache.flush() // clear the whole store ``` | Method | Description | @@ -125,8 +125,8 @@ await Cache.flush(); // clear the whole store Target a specific store with `store()`: ```ts -await Cache.store('redis').forever('flag', true); -await Cache.store('file').remember('report', 3600, buildReport); +await Cache.store('redis').forever('flag', true) +await Cache.store('file').remember('report', 3600, buildReport) ``` ## Console @@ -143,13 +143,13 @@ ark cache:clear --store=redis Implement the `Store` contract and register it with `Cache.extend`: ```ts -import { Cache, Store } from '@arkstack/cache'; +import { Cache, Store } from '@arkstack/cache' class TagStore extends Store { /* implement get/put/forever/increment/decrement/forget/flush/getPrefix */ } -Cache.extend('tags', (config) => new TagStore(/* ... */)); +Cache.extend('tags', (config) => new TagStore(/* ... */)) ``` Then reference it in a store config: `{ driver: 'tags' }`. diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 4918261e..9eb525fa 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -106,7 +106,7 @@ pnpm ark publish --tag queue-migrations --force Packages register what they publish by calling `Publisher.publishes()` from their `setup` module: ```ts -import { Publisher } from '@arkstack/common'; +import { Publisher } from '@arkstack/common' Publisher.publishes({ package: '@arkstack/cache', @@ -117,7 +117,7 @@ Publisher.publishes({ to: 'src/database/migrations/create_cache_table.ts', }, ], -}); +}) ``` Source artifacts may be named with a trailing `.stub` extension (e.g. `Model.ts.stub`) so they are ignored by the package's own linting, type-checking and tests. The `.stub` suffix is stripped automatically when the file is published, restoring its real extension. @@ -127,11 +127,19 @@ Source artifacts may be named with a trailing `.stub` extension (e.g. `Model.ts. A package can take more control over **what** and **how** it publishes by registering a confirmation with `Publisher.confirm()`. When you run `ark publish`, the user is prompted to choose an option; the chosen value selects which tagged group is published, and an optional `callback` post-processes each published stub. ```ts -import { Publisher } from '@arkstack/common'; +import { Publisher } from '@arkstack/common' // Register the framework-specific groups behind tags… -Publisher.publishes({ package: '@arkstack/inertia', tag: 'inertia-vue', entries: [/* … */] }); -Publisher.publishes({ package: '@arkstack/inertia', tag: 'inertia-react', entries: [/* … */] }); +Publisher.publishes({ + package: '@arkstack/inertia', + tag: 'inertia-vue', + entries: [/* … */], +}) +Publisher.publishes({ + package: '@arkstack/inertia', + tag: 'inertia-react', + entries: [/* … */], +}) // …then let the user pick one. Publisher.confirm({ @@ -143,7 +151,7 @@ Publisher.confirm({ ], // Transform every stub this package publishes based on the choice. callback: (choice, stub) => stub.replaceAll('{{ext}}', choice === 'inertia-react' ? '.tsx' : '.ts'), -}); +}) ``` How it behaves: diff --git a/docs/guide/database-modeling.md b/docs/guide/database-modeling.md index 5edb2b32..4e0238e6 100644 --- a/docs/guide/database-modeling.md +++ b/docs/guide/database-modeling.md @@ -23,14 +23,14 @@ pnpm ark migrate Create an Arkormˣ model entry in `src/app/models` (example `User.ts`): ```ts -import { Model } from 'arkormx'; +import { Model } from 'arkormx' export class User extends Model { - declare id: string; - declare name: string; - declare email: string; - declare createdAt: Date; - declare updatedAt: Date; + declare id: string + declare name: string + declare email: string + declare createdAt: Date + declare updatedAt: Date } ``` @@ -58,14 +58,14 @@ Use the model query API for common operations: const user = await User.query().create({ name: 'John Doe', email: 'john@example.com', -}); +}) -const found = await User.query().where({ email: 'john@example.com' }).first(); +const found = await User.query().where({ email: 'john@example.com' }).first() if (found) { - found.name = 'Jane Doe'; - await found.save(); - await found.delete(); + found.name = 'Jane Doe' + await found.save() + await found.delete() } ``` @@ -74,21 +74,21 @@ if (found) { Shared Arkstack packages use `getModel()` from `@arkstack/common` when they need an application model without importing from a fixed app path. ```ts -import { getModel } from '@arkstack/common'; -import type User from '../app/models/User'; +import { getModel } from '@arkstack/common' +import type User from '../app/models/User' -const UserModel = await getModel('User'); -const user = await UserModel.query().where({ email }).first(); +const UserModel = await getModel('User') +const user = await UserModel.query().where({ email }).first() ``` Apps can also augment the model registry to get typed model names without passing the constructor type each time: ```ts -import type User from './src/app/models/User'; +import type User from './src/app/models/User' declare module '@arkstack/common' { interface ModelRegistry { - User: typeof User; + User: typeof User } } ``` diff --git a/docs/guide/deployment.md b/docs/guide/deployment.md index 243ffbfc..7b62e670 100644 --- a/docs/guide/deployment.md +++ b/docs/guide/deployment.md @@ -49,12 +49,12 @@ All three are exported from `@arkstack/common`. Resolve an application module's **source path** to a file that can be imported at runtime. Production prefers the compiled output; development prefers source. The leading `src/` segment is remapped to the output directory and the correct extension is chosen (`.ts` source → `.js` output). Returns the absolute source path when nothing exists, so a missing module fails with a clear error. ```ts -import { resolveRuntimeModule, importFile } from '@arkstack/common'; +import { resolveRuntimeModule, importFile } from '@arkstack/common' // dev -> /src/app/models/User.ts // prod -> /dist/app/models/User.js -const path = resolveRuntimeModule('src/app/models/User'); -const module = await importFile(path); +const path = resolveRuntimeModule('src/app/models/User') +const module = await importFile(path) ``` ### `resolveRuntimeDir(sourcePath)` @@ -62,11 +62,11 @@ const module = await importFile(path); The directory counterpart of `resolveRuntimeModule`: maps a **source directory** to its build-output location without any file-extension handling. Returns the absolute source directory when neither exists. ```ts -import { resolveRuntimeDir } from '@arkstack/common'; +import { resolveRuntimeDir } from '@arkstack/common' // dev -> /src/routes // prod -> /dist/routes -const dir = resolveRuntimeDir('src/routes'); +const dir = resolveRuntimeDir('src/routes') ``` ### `toOutputPath(sourcePath)` @@ -74,10 +74,10 @@ const dir = resolveRuntimeDir('src/routes'); Map a source path to its build-output counterpart: strips the leading `src/` and rewrites a TypeScript source extension to `.js` (directories keep their shape). It's a **pure path transform** — it does not check the filesystem. ```ts -import { toOutputPath } from '@arkstack/common'; +import { toOutputPath } from '@arkstack/common' -toOutputPath('src/app/models/User.ts'); // /dist/app/models/User.js (prod) -toOutputPath('src/routes'); // /dist/routes (prod) +toOutputPath('src/app/models/User.ts') // /dist/app/models/User.js (prod) +toOutputPath('src/routes') // /dist/routes (prod) ``` > `resolveRuntimeModule` / `resolveRuntimeDir` consult the filesystem and pick the existing file/dir for the current environment; `toOutputPath` just computes the mapped location. diff --git a/docs/guide/express-runtime.md b/docs/guide/express-runtime.md index d690aebd..ac132b88 100644 --- a/docs/guide/express-runtime.md +++ b/docs/guide/express-runtime.md @@ -10,7 +10,7 @@ From `src/core/bootstrap.ts`: - `app`: Arkstack `Application` instance ```ts -import { app, expressApp } from 'src/core/bootstrap'; +import { app, expressApp } from 'src/core/bootstrap' ``` ## Interaction Options @@ -20,11 +20,11 @@ import { app, expressApp } from 'src/core/bootstrap'; Use this for direct Express APIs. ```ts -import { expressApp } from 'src/core/bootstrap'; +import { expressApp } from 'src/core/bootstrap' -expressApp.set('trust proxy', 1); -expressApp.disable('x-powered-by'); -expressApp.locals.serviceName = 'api'; +expressApp.set('trust proxy', 1) +expressApp.disable('x-powered-by') +expressApp.locals.serviceName = 'api' ``` ### 2. Instance runtime accessor: `app.getAppInstance()` @@ -32,13 +32,13 @@ expressApp.locals.serviceName = 'api'; Get the runtime instance from the Arkstack wrapper. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const runtime = app.getAppInstance(); +const runtime = app.getAppInstance() runtime.use((req, _res, next) => { - req.headers['x-runtime'] = 'express'; - next(); -}); + req.headers['x-runtime'] = 'express' + next() +}) ``` ### 3. Static runtime accessor: `Application.getAppInstance()` @@ -46,10 +46,10 @@ runtime.use((req, _res, next) => { Access the static runtime reference. ```ts -import Application from 'src/core/app'; +import Application from 'src/core/app' -const runtime = Application.getAppInstance(); -runtime.set('etag', false); +const runtime = Application.getAppInstance() +runtime.set('etag', false) ``` ### 4. Driver interaction: `app.getDriver()` @@ -57,11 +57,11 @@ runtime.set('etag', false); Use the runtime driver for lifecycle-level integration. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const driver = app.getDriver(); -const runtime = app.getAppInstance(); -await driver.applyMiddleware(runtime, (req, _res, next) => next()); +const driver = app.getDriver() +const runtime = app.getAppInstance() +await driver.applyMiddleware(runtime, (req, _res, next) => next()) ``` ### 5. Router contract interaction: `app.getRouter()` @@ -69,23 +69,23 @@ await driver.applyMiddleware(runtime, (req, _res, next) => next()); Use the runtime-agnostic router contract. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const router = app.getRouter(); -await router.bind(app.getAppInstance()); +const router = app.getRouter() +await router.bind(app.getAppInstance()) -const routes = await router.list({ path: '/api' }); -console.log(routes); +const routes = await router.list({ path: '/api' }) +console.log(routes) ``` ### 6. Lifecycle control: `boot` and `shutdown` ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -await app.boot(3000); +await app.boot(3000) // later -await app.shutdown(); +await app.shutdown() ``` ## Unified HTTP Context @@ -94,14 +94,14 @@ The Express driver automatically installs `arkstackHttpPlugin`. Clear Router han ```ts Router.get('/account', ({ req, res, clearRequest, clearResponse }) => { - const token = clearRequest.bearerToken(); + const token = clearRequest.bearerToken() return clearResponse.status(200).json({ ip: req.ip, token, headersSent: res.headersSent, - }); -}); + }) +}) ``` Controllers decorated with `@Bind()` can inject `Request`, `Response`, and `Session` from `@arkstack/http`. The injected values are the same instances exposed on the route context and by the global HTTP helpers. See the [HTTP guide](/guide/http#controller-injection). @@ -119,13 +119,13 @@ Default behavior: If you need different static asset behavior, override `mountPublicAssets` in `src/core/app.ts` when constructing `ExpressDriver`. ```ts -import express from 'express'; -import path from 'path'; -import { ExpressDriver } from '@arkstack/driver-express'; +import express from 'express' +import path from 'path' +import { ExpressDriver } from '@arkstack/driver-express' this.driver = new ExpressDriver({ bindRouter: async (runtime) => { - runtime.use(await Router.bind()); + runtime.use(await Router.bind()) }, mountPublicAssets: (runtime, publicPath) => { runtime.use( @@ -133,10 +133,10 @@ this.driver = new ExpressDriver({ express.static(path.resolve(publicPath), { maxAge: '7d', }), - ); + ) }, errorHandler: ErrorHandler, -}); +}) ``` ## Host & Port Binding diff --git a/docs/guide/h3-runtime.md b/docs/guide/h3-runtime.md index 05b5c568..4326ef4c 100644 --- a/docs/guide/h3-runtime.md +++ b/docs/guide/h3-runtime.md @@ -10,7 +10,7 @@ From `src/core/bootstrap.ts`: - `app`: Arkstack `Application` instance ```ts -import { app, h3App } from 'src/core/bootstrap'; +import { app, h3App } from 'src/core/bootstrap' ``` ## Interaction Options @@ -20,12 +20,12 @@ import { app, h3App } from 'src/core/bootstrap'; Use this for direct H3 APIs. ```ts -import { h3App } from 'src/core/bootstrap'; -import { HTTPResponse } from 'h3'; +import { h3App } from 'src/core/bootstrap' +import { HTTPResponse } from 'h3' h3App.get('/health', () => { - return new HTTPResponse('ok'); -}); + return new HTTPResponse('ok') +}) ``` ### 2. Instance runtime accessor: `app.getAppInstance()` @@ -33,12 +33,12 @@ h3App.get('/health', () => { Get the runtime instance from the Arkstack wrapper. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const runtime = app.getAppInstance(); +const runtime = app.getAppInstance() runtime.use((event) => { - event.context.source = 'arkstack'; -}); + event.context.source = 'arkstack' +}) ``` ### 3. Static runtime accessor: `Application.getAppInstance()` @@ -46,12 +46,12 @@ runtime.use((event) => { Access the static runtime reference. ```ts -import Application from 'src/core/app'; +import Application from 'src/core/app' -const runtime = Application.getAppInstance(); +const runtime = Application.getAppInstance() runtime.use(() => { // global middleware hook -}); +}) ``` ### 4. Driver interaction: `app.getDriver()` @@ -59,11 +59,11 @@ runtime.use(() => { Use the runtime driver for lifecycle-level integration. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const driver = app.getDriver(); -const runtime = app.getAppInstance(); -await driver.applyMiddleware(runtime, () => {}); +const driver = app.getDriver() +const runtime = app.getAppInstance() +await driver.applyMiddleware(runtime, () => {}) ``` ### 5. Router contract interaction: `app.getRouter()` @@ -71,23 +71,23 @@ await driver.applyMiddleware(runtime, () => {}); Use the runtime-agnostic router contract. ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -const router = app.getRouter(); -await router.bind(app.getAppInstance()); +const router = app.getRouter() +await router.bind(app.getAppInstance()) -const routes = await router.list({ path: '/api' }, app.getAppInstance()); -console.log(routes); +const routes = await router.list({ path: '/api' }, app.getAppInstance()) +console.log(routes) ``` ### 6. Lifecycle control: `boot` and `shutdown` ```ts -import { app } from 'src/core/bootstrap'; +import { app } from 'src/core/bootstrap' -await app.boot(3000); +await app.boot(3000) // later -await app.shutdown(); +await app.shutdown() ``` ## Unified HTTP Context @@ -96,13 +96,13 @@ The H3 driver automatically installs `arkstackHttpPlugin`. Clear Router handlers ```ts Router.get('/account', ({ req, clearRequest, clearResponse }) => { - const token = clearRequest.bearerToken(); + const token = clearRequest.bearerToken() return clearResponse.status(200).json({ method: req.method, token, - }); -}); + }) +}) ``` Controllers decorated with `@Bind()` can inject `Request`, `Response`, and `Session` from `@arkstack/http`. The injected values are the same instances exposed on the route context and by the global HTTP helpers. See the [HTTP guide](/guide/http#controller-injection). @@ -121,18 +121,18 @@ Default behavior: If you need to change that behavior, override `mountPublicAssets` in `src/core/app.ts` when constructing `H3Driver`. ```ts -import { H3Driver } from '@arkstack/driver-h3'; -import { staticAssetHandler } from '@arkstack/driver-h3/middlewares'; +import { H3Driver } from '@arkstack/driver-h3' +import { staticAssetHandler } from '@arkstack/driver-h3/middlewares' this.driver = new H3Driver({ createApp: () => new H3({ onError: ErrorHandler }), bindRouter: async (runtime) => { - await Router.bind(runtime); + await Router.bind(runtime) }, mountPublicAssets: (runtime, publicPath) => { - runtime.use(staticAssetHandler(publicPath)); + runtime.use(staticAssetHandler(publicPath)) }, -}); +}) ``` ## Host & Port Binding diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index 76eb08c9..4c868b46 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -7,16 +7,16 @@ Arkstack exposes a small shared hook registry through `@arkstack/foundry`. Hooks Use `Hook.set(name, positions)` to register callbacks. The built-in positions are `before` and `after`, and applications can use custom position names when a package documents them. ```ts -import { Hook } from '@arkstack/foundry'; +import { Hook } from '@arkstack/foundry' Hook.set('middleware:auth', { before: (context) => { - context.logger?.debug('auth middleware starting'); + context.logger?.debug('auth middleware starting') }, after: (context) => { - context.logger?.debug('auth middleware finished'); + context.logger?.debug('auth middleware finished') }, -}); +}) ``` Calling `set()` again for the same hook name merges positions and only overwrites the positions supplied in the new registration. @@ -24,9 +24,9 @@ Calling `set()` again for the same hook name merges positions and only overwrite ```ts Hook.set('middleware:auth', { error: (error) => { - console.error(error); + console.error(error) }, -}); +}) ``` ## Read And Run Hooks @@ -34,15 +34,15 @@ Hook.set('middleware:auth', { Use `Hook.has()` to check for a hook and `Hook.get()` to retrieve either the full hook object or one position. ```ts -const beforeAuth = Hook.get('middleware:auth', 'before'); +const beforeAuth = Hook.get('middleware:auth', 'before') -beforeAuth?.({ logger }); +beforeAuth?.({ logger }) ``` You can also read every registered hook: ```ts -const hooks = Hook.getAll(); +const hooks = Hook.getAll() ``` ## Remove Hooks @@ -50,9 +50,9 @@ const hooks = Hook.getAll(); Unset a single hook, a single position, or every registered hook: ```ts -Hook.unset('middleware:auth', 'before'); -Hook.unset('middleware:auth'); -Hook.clear(); +Hook.unset('middleware:auth', 'before') +Hook.unset('middleware:auth') +Hook.clear() ``` Hooks are process-local. Register them during application boot or test setup so their lifecycle is explicit. diff --git a/docs/guide/http-testing.md b/docs/guide/http-testing.md index ab199053..82bcb104 100644 --- a/docs/guide/http-testing.md +++ b/docs/guide/http-testing.md @@ -9,18 +9,15 @@ Generated applications include Parasito as a development dependency and use it w Pass the Arkstack application exported by your bootstrap directly to `request()`. You do not need to start a listening server: ```ts -import { describe, it } from 'vitest'; -import request from 'parasito'; -import { app } from '../src/core/bootstrap'; +import { describe, it } from 'vitest' +import request from 'parasito' +import { app } from '../src/core/bootstrap' describe('users API', () => { it('returns users', async () => { - await request(app) - .get('/api/users') - .expect(200) - .expect('content-type', /json/); - }); -}); + await request(app).get('/api/users').expect(200).expect('content-type', /json/) + }) +}) ``` The same test shape works with both Arkstack drivers. @@ -44,7 +41,7 @@ await request(app) name: 'Ada Lovelace', email: 'ada@example.com', }, - }); + }) ``` `send()` selects JSON, form, multipart, or text content types from the supplied body. Set `content-type` explicitly when a test needs to override that behavior. @@ -54,13 +51,13 @@ await request(app) Await a request to inspect the normalized response with your test runner's assertions: ```ts -import { expect } from 'vitest'; +import { expect } from 'vitest' -const response = await request(app).get('/api/users/1'); +const response = await request(app).get('/api/users/1') -expect(response.status).toBe(200); -expect(response.body.data.id).toBe(1); -expect(response.headers.get('content-type')).toMatch(/json/); +expect(response.status).toBe(200) +expect(response.body.data.id).toBe(1) +expect(response.headers.get('content-type')).toMatch(/json/) ``` Responses expose `status`, `statusCode`, `ok`, `headers`, `header`, `text`, `body`, and the underlying `raw` response. @@ -70,4 +67,3 @@ Responses expose `status`, `statusCode`, `ok`, `headers`, `header`, `text`, `bod Parasito can also test Express, H3, Fastify, Hono, Koa, Node handlers, fetch-style applications, listening servers, and remote URLs. This is useful when an Arkstack package or runtime adapter needs to be tested outside a complete application. See the [Parasito reference and examples](https://github.com/arkstack-hq/parasito#readme) for request bodies, target adapters, remote URLs, and the full assertion API. - diff --git a/docs/guide/http.md b/docs/guide/http.md index 2406c444..e2880a27 100644 --- a/docs/guide/http.md +++ b/docs/guide/http.md @@ -9,7 +9,7 @@ The Express and H3 drivers automatically bind these wrappers to each Clear Route Create a normalized request from a native request-like object: ```ts -import { Request } from '@arkstack/http'; +import { Request } from '@arkstack/http' const request = Request.from({ headers: { @@ -18,18 +18,18 @@ const request = Request.from({ method: 'GET', path: '/account', url: 'https://example.test/account', -}); +}) -request.header('authorization'); -request.bearerToken(); +request.header('authorization') +request.bearerToken() ``` Requests can also carry the authenticated user: ```ts -request.setUser(user); +request.setUser(user) -const user = request.user; +const user = request.user ``` ## Response @@ -37,17 +37,17 @@ const user = request.user; Use `Response` when shared code needs a consistent response shape: ```ts -import { Response } from '@arkstack/http'; +import { Response } from '@arkstack/http' const response = new Response({ statusCode: 200, headers: { 'content-type': 'application/json', }, -}); +}) -response.status(201); -response.json({ ok: true }); +response.status(201) +response.json({ ok: true }) ``` ## Route Context @@ -56,10 +56,10 @@ Every route context receives one Arkstack request and response instance for the ```ts Router.put('/api/users/:id', ({ clearRequest, clearResponse }) => { - const id = clearRequest.param('id'); + const id = clearRequest.param('id') - return clearResponse.status(200).json({ id }); -}); + return clearResponse.status(200).json({ id }) +}) ``` `clearRequest` and `clearResponse` are the unified Arkstack HTTP objects. Native objects remain available alongside them: Express handlers receive `req` and `res`, while H3 handlers retain the native H3 request and event context. @@ -69,15 +69,15 @@ Router.put('/api/users/:id', ({ clearRequest, clearResponse }) => { Arkstack enables Clear Router's container, decorator metadata, and automatic discovery during normal application boot. Decorate a controller method with `@Bind()` to resolve its typed arguments from the current request scope: ```ts -import { Request, Response, Session } from '@arkstack/http'; -import { Bind } from 'clear-router/decorators'; +import { Request, Response, Session } from '@arkstack/http' +import { Bind } from 'clear-router/decorators' export default class ProfileController { @Bind() async show(request: Request, response: Response, session: Session) { - session.put('profile', request.param('id')); + session.put('profile', request.param('id')) - return response.status(200).json({ id: request.param('id') }); + return response.status(200).json({ id: request.param('id') }) } } ``` @@ -85,7 +85,7 @@ export default class ProfileController { Register decorated controller methods with the regular controller tuple syntax: ```ts -Router.get('/profiles/:id', [ProfileController, 'show']); +Router.get('/profiles/:id', [ProfileController, 'show']) ``` Repeated resolution of `Request`, `Response`, or `Session` within one route returns the same instance. Each concurrent request receives its own instances. @@ -97,11 +97,11 @@ Applications using Clear Router outside the Arkstack drivers can install the int The global helpers resolve against the active request scope: ```ts -request(); // current Request -request('email'); // one input value -response(); // current Response -session(); // current Session -session('notice'); // one session value +request() // current Request +request('email') // one input value +response() // current Response +session() // current Session +session('notice') // one session value ``` The helpers are safe across concurrent routed requests. Resolve them where they are used rather than storing a request-scoped value for later reuse. @@ -111,18 +111,18 @@ The helpers are safe across concurrent routed requests. Resolve them where they The HTTP package includes a request session container for framework-neutral route handlers. Arkstack starters import the setup entry during application boot. Custom bootstraps should import it once so sessions, validation integration, and decorator support are initialized: ```ts -import '@arkstack/http/setup'; +import '@arkstack/http/setup' ``` Inside a Clear Router handler, the context receives `session`, `httpSession`, and `errors`: ```ts Router.post('/profile', async ({ session }) => { - session.put('intended', '/dashboard'); - session.addError('email', 'Email is required'); + session.put('intended', '/dashboard') + session.addError('email', 'Email is required') - return { ok: false }; -}); + return { ok: false } +}) ``` Use `httpSession` when another package already owns a `session` property on the context. Arkstack preserves existing non-HTTP sessions and exposes its own container as `httpSession`. @@ -130,16 +130,16 @@ Use `httpSession` when another package already owns a `session` property on the The session API supports the following common bag methods: ```ts -session.get('intended'); -session.put('notice', 'Saved'); -session.has('notice'); -session.forget('notice'); -session.clear(); - -session.addError('email', 'Email is required'); -session.addErrors({ password: ['Password is too short'] }); -session.hasErrors('email'); -session.clearErrors('email'); +session.get('intended') +session.put('notice', 'Saved') +session.has('notice') +session.forget('notice') +session.clear() + +session.addError('email', 'Email is required') +session.addErrors({ password: ['Password is too short'] }) +session.hasErrors('email') +session.clearErrors('email') ``` ### Session Persistence @@ -161,7 +161,7 @@ export default () => ({ database: { table: env('SESSION_TABLE', 'sessions'), }, -}); +}) ``` Available drivers: @@ -173,14 +173,14 @@ Available drivers: You can also configure sessions programmatically: ```ts -import { FileSessionDriver, configureSession } from '@arkstack/http'; +import { FileSessionDriver, configureSession } from '@arkstack/http' configureSession( new FileSessionDriver({ directory: 'storage/framework/sessions', secret: process.env.SESSION_SECRET, }), -); +) ``` Session mutations are persisted automatically. You may call `await session.save()` when a test or custom integration needs to wait for the write explicitly. @@ -190,8 +190,8 @@ Session mutations are persisted automatically. You may call `await session.save( Use the session flash bag for data that should survive exactly one following request. Flashed values are available through `session.flashBag` and are swept by the `web` middleware before the response completes: ```ts -session.flash('notice', 'Profile saved'); -session.getFlash('notice'); +session.flash('notice', 'Profile saved') +session.getFlash('notice') ``` The error bag extends the same flash behavior, so validation errors survive a redirect and are cleared after the response that consumes them. @@ -201,23 +201,23 @@ The error bag extends the same flash behavior, so validation errors survive a re Arkstack uses Kanun for validation. Importing `@arkstack/http/setup` registers the Kanun session plugin, so failed validators automatically fill the current HTTP session error bag. You can still pass a Kanun validator message bag, a Kanun validation exception, or a plain keyed error object into the session manually when needed: ```ts -import { Validator, ValidationException } from 'kanun'; +import { Validator, ValidationException } from 'kanun' const validator = Validator.make(body, { email: 'required|email', -}); +}) if (await validator.fails()) { // The Kanun session plugin has already copied validator.errors() // into the current request session. - return await view('profile.edit'); + return await view('profile.edit') } try { - await validator.validate(); + await validator.validate() } catch (error) { if (error instanceof ValidationException) { - session.addValidationErrors(error); + session.addValidationErrors(error) } } ``` @@ -227,16 +227,20 @@ The error bag is available as `session.errors` and as `errors` on the HTTP conte For browser form routes, add the `web` middleware. Validation errors on those routes redirect back to the source route and flash the errors into the session. During the submitted request, `old()` reads directly from the current request input: ```ts -import { old, redirect, web } from '@arkstack/http'; +import { old, redirect, web } from '@arkstack/http' -Router.post('/register', async ({ req }) => { - await validator.validate(); +Router.post( + '/register', + async ({ req }) => { + await validator.validate() - return redirect('/dashboard'); -}, [web]); + return redirect('/dashboard') + }, + [web], +) -old(); // all submitted form data from the current request -old('email'); // one field from the current request input +old() // all submitted form data from the current request +old('email') // one field from the current request input ``` When `@arkstack/view/setup` is also imported, the current session and error bag are available to Edge views rendered during the request. diff --git a/docs/guide/inertia.md b/docs/guide/inertia.md index cb02cec6..b258b3d6 100644 --- a/docs/guide/inertia.md +++ b/docs/guide/inertia.md @@ -30,28 +30,28 @@ Add the `inertia()` middleware to `src/config/middleware.ts` so the adapter can ```ts [Express] // src/config/middleware.ts -import { inertia, resora } from '@arkstack/driver-express/middlewares'; +import { inertia, resora } from '@arkstack/driver-express/middlewares' -import { MiddlewareConfig } from '@arkstack/driver-express/types'; +import { MiddlewareConfig } from '@arkstack/driver-express/types' export default (): MiddlewareConfig => { return { before: [resora(), inertia()], - }; -}; + } +} ``` ```ts [H3] // src/config/middleware.ts -import { inertia, resora } from '@arkstack/driver-h3/middlewares'; +import { inertia, resora } from '@arkstack/driver-h3/middlewares' -import { MiddlewareConfig } from '@arkstack/driver-h3/types'; +import { MiddlewareConfig } from '@arkstack/driver-h3/types' export default (): MiddlewareConfig => { return { before: [resora(), inertia()], - }; -}; + } +} ``` ::: @@ -112,8 +112,8 @@ Add a `vite.config.ts`. Build into `public/build` (the framework serves `public/ ::: code-group ```ts [Vue] -import { defineConfig } from 'vite'; -import vue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], @@ -121,14 +121,16 @@ export default defineConfig({ build: { manifest: true, outDir: 'public/build', - rolldownOptions: { input: ['resources/css/app.css', 'resources/js/app.ts'] }, + rolldownOptions: { + input: ['resources/css/app.css', 'resources/js/app.ts'], + }, }, -}); +}) ``` ```ts [React] -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], @@ -136,14 +138,16 @@ export default defineConfig({ build: { manifest: true, outDir: 'public/build', - rolldownOptions: { input: ['resources/css/app.css', 'resources/js/app.tsx'] }, + rolldownOptions: { + input: ['resources/css/app.css', 'resources/js/app.tsx'], + }, }, -}); +}) ``` ```ts [Svelte] -import { defineConfig } from 'vite'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ plugins: [svelte()], @@ -151,9 +155,11 @@ export default defineConfig({ build: { manifest: true, outDir: 'public/build', - rolldownOptions: { input: ['resources/css/app.css', 'resources/js/app.ts'] }, + rolldownOptions: { + input: ['resources/css/app.css', 'resources/js/app.ts'], + }, }, -}); +}) ``` ::: @@ -187,52 +193,52 @@ Create the client entry that boots Inertia and resolves your pages from `resourc ```ts [Vue] // resources/js/app.ts -import { createApp, h } from 'vue'; -import { createInertiaApp } from '@inertiajs/vue3'; +import { createApp, h } from 'vue' +import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }); - return pages[`./Pages/${name}.vue`]; + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) - .mount(el); + .mount(el) }, -}); +}) ``` ```tsx [React] // resources/js/app.tsx -import { createInertiaApp } from '@inertiajs/react'; -import { createRoot } from 'react-dom/client'; +import { createInertiaApp } from '@inertiajs/react' +import { createRoot } from 'react-dom/client' createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }); - return pages[`./Pages/${name}.tsx`]; + const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) + return pages[`./Pages/${name}.tsx`] }, setup({ el, App, props }) { - createRoot(el).render(); + createRoot(el).render() }, -}); +}) ``` ```ts [Svelte] // resources/js/app.ts -import { createInertiaApp } from '@inertiajs/svelte'; -import { mount } from 'svelte'; +import { createInertiaApp } from '@inertiajs/svelte' +import { mount } from 'svelte' createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }); - return pages[`./Pages/${name}.svelte`]; + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] }, setup({ el, App, props }) { - mount(App, { target: el, props }); + mount(App, { target: el, props }) }, -}); +}) ``` ::: @@ -244,14 +250,14 @@ Run Vite alongside `ark dev` during development (e.g. `vite`), and `vite build` Return `inertia(component, props)` (or `Inertia.render(...)`) from a controller. The same call produces an HTML document on the first visit and a JSON page object on Inertia visits. ```ts -import { inertia } from '@arkstack/inertia'; -import { User } from '@app/models/User'; +import { inertia } from '@arkstack/inertia' +import { User } from '@app/models/User' export class UserController { async index() { return inertia('Users/Index', { users: await User.all(), - }); + }) } } ``` @@ -263,13 +269,13 @@ export class UserController { Share props with every response globally at boot, or per request inside a handler: ```ts -import { inertia } from '@arkstack/inertia'; +import { inertia } from '@arkstack/inertia' // At boot — applies to every request -inertia().share('appName', config('app.name')); +inertia().share('appName', config('app.name')) // Inside a request — scoped to this request only -inertia().share({ flash: session.get('flash') }); +inertia().share({ flash: session.get('flash') }) ``` Page props override shared props of the same name. @@ -295,7 +301,7 @@ return inertia('Users/Index', { chart: Inertia.defer(() => buildChart()), // always present, even on partial reloads auth: Inertia.always({ user: request.user }), -}); +}) ``` ## Redirects @@ -323,8 +329,8 @@ export default (): InertiaConfig => { root_view: 'app', version: env('INERTIA_VERSION', null), ssr: { enabled: false }, - }; -}; + } +} ``` You can also set it at runtime: `Inertia.version(() => buildHash())`. @@ -354,60 +360,60 @@ Create a server entry (`resources/js/ssr.ts`) that renders a page to `{ head, bo ```ts [Vue] // resources/js/ssr.ts -import { createInertiaApp } from '@inertiajs/vue3'; -import createServer from '@inertiajs/vue3/server'; -import { renderToString } from '@vue/server-renderer'; -import { createSSRApp, h } from 'vue'; +import { createInertiaApp } from '@inertiajs/vue3' +import createServer from '@inertiajs/vue3/server' +import { renderToString } from '@vue/server-renderer' +import { createSSRApp, h } from 'vue' createServer((page) => createInertiaApp({ page, render: renderToString, resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }); - return pages[`./Pages/${name}.vue`]; + const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) + return pages[`./Pages/${name}.vue`] }, setup({ App, props, plugin }) { - return createSSRApp({ render: () => h(App, props) }).use(plugin); + return createSSRApp({ render: () => h(App, props) }).use(plugin) }, }), -); +) ``` ```tsx [React] // resources/js/ssr.tsx -import { createInertiaApp } from '@inertiajs/react'; -import createServer from '@inertiajs/react/server'; -import ReactDOMServer from 'react-dom/server'; +import { createInertiaApp } from '@inertiajs/react' +import createServer from '@inertiajs/react/server' +import ReactDOMServer from 'react-dom/server' createServer((page) => createInertiaApp({ page, render: ReactDOMServer.renderToString, resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }); - return pages[`./Pages/${name}.tsx`]; + const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) + return pages[`./Pages/${name}.tsx`] }, setup: ({ App, props }) => , }), -); +) ``` ```ts [Svelte] // resources/js/ssr.ts -import { createInertiaApp } from '@inertiajs/svelte'; -import createServer from '@inertiajs/svelte/server'; +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' createServer((page) => createInertiaApp({ page, resolve: (name) => { - const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }); - return pages[`./Pages/${name}.svelte`]; + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] }, setup: ({ App, props }) => App.render(props), }), -); +) ``` ::: diff --git a/docs/guide/jobs.md b/docs/guide/jobs.md index 4dcccbac..99e56652 100644 --- a/docs/guide/jobs.md +++ b/docs/guide/jobs.md @@ -27,7 +27,7 @@ yarn add @arkstack/jobs @arkstack/queue Importing `@arkstack/jobs` anywhere wires the queue (de)serialization automatically. For an explicit bootstrap hook — alongside `@arkstack/database/setup` — import the setup entry: ```ts -import '@arkstack/jobs/setup'; +import '@arkstack/jobs/setup' ``` ## Writing a job @@ -42,11 +42,11 @@ This creates `src/app/jobs/SendWelcomeEmail.ts`. Extend `Job` and implement `han ```ts // src/app/jobs/SendWelcomeEmail.ts -import { Job } from '@arkstack/jobs'; +import { Job } from '@arkstack/jobs' export class SendWelcomeEmail extends Job { constructor(public userId: number) { - super(); + super() } async handle() { @@ -60,17 +60,17 @@ A job's constructor arguments become its serialized state, so keep them to plain ## Dispatching ```ts -import { dispatch } from '@arkstack/jobs'; -import { SendWelcomeEmail } from '@app/jobs/SendWelcomeEmail'; +import { dispatch } from '@arkstack/jobs' +import { SendWelcomeEmail } from '@app/jobs/SendWelcomeEmail' // static helper -await SendWelcomeEmail.dispatch(user.id); -await SendWelcomeEmail.dispatch(user.id).onQueue('mail').withDelay(60); -await SendWelcomeEmail.dispatchSync(user.id); // run inline now +await SendWelcomeEmail.dispatch(user.id) +await SendWelcomeEmail.dispatch(user.id).onQueue('mail').withDelay(60) +await SendWelcomeEmail.dispatchSync(user.id) // run inline now // functional helper -await dispatch(new SendWelcomeEmail(user.id)); -await dispatch(new SendWelcomeEmail(user.id), { queue: 'mail', delay: 60 }); +await dispatch(new SendWelcomeEmail(user.id)) +await dispatch(new SendWelcomeEmail(user.id), { queue: 'mail', delay: 60 }) ``` The pending dispatch is awaitable and chainable: @@ -92,14 +92,14 @@ ark queue:work Control attempts and backoff with instance properties, and react to permanent failure with a `failed` hook: ```ts -import { Job } from '@arkstack/jobs'; +import { Job } from '@arkstack/jobs' export class ChargeInvoice extends Job { - tries = 3; // max attempts before the job is marked failed - backoff = 30; // seconds to wait before a released job retries + tries = 3 // max attempts before the job is marked failed + backoff = 30 // seconds to wait before a released job retries constructor(public invoiceId: number) { - super(); + super() } async handle() { @@ -125,9 +125,9 @@ serialize() { A dedicated worker **process** constructs none of your jobs, so `ark queue:work` imports every job module in `src/app/jobs` (or its build output) before it starts working, registering each class it finds under its class name. Jobs kept elsewhere can be registered by loading that directory instead: ```ts -import { loadJobs } from '@arkstack/jobs'; +import { loadJobs } from '@arkstack/jobs' -await loadJobs('domain/jobs'); // relative to src/, or the build output +await loadJobs('domain/jobs') // relative to src/, or the build output ``` `JobRegistry.register(MyJob)` still registers a single class explicitly. diff --git a/docs/guide/middleware.md b/docs/guide/middleware.md index 6389539c..3c14a938 100644 --- a/docs/guide/middleware.md +++ b/docs/guide/middleware.md @@ -43,13 +43,13 @@ Express example: ```ts this.driver = new ExpressDriver({ bindRouter: async (runtime) => { - runtime.use(await Router.bind()); + runtime.use(await Router.bind()) }, mountPublicAssets: (runtime, publicPath) => { - runtime.use('/assets', express.static(publicPath)); + runtime.use('/assets', express.static(publicPath)) }, errorHandler: ErrorHandler, -}); +}) ``` H3 example: @@ -58,12 +58,12 @@ H3 example: this.driver = new H3Driver({ createApp: () => new H3({ onError: ErrorHandler }), bindRouter: async (runtime) => { - await Router.bind(runtime); + await Router.bind(runtime) }, mountPublicAssets: (runtime, publicPath) => { - runtime.use(staticAssetHandler(publicPath)); + runtime.use(staticAssetHandler(publicPath)) }, -}); +}) ``` Use each group with this intent: @@ -77,17 +77,17 @@ Use each group with this intent: `express/src/config/middleware.ts`: ```ts -import express, { Express } from 'express'; -import cors from 'cors'; -import { MiddlewareConfig } from 'src/types/config'; +import express, { Express } from 'express' +import cors from 'cors' +import { MiddlewareConfig } from 'src/types/config' export default (_app: Express): MiddlewareConfig => { return { global: [express.json(), express.urlencoded({ extended: true }), cors()], before: [], after: [], - }; -}; + } +} ``` ## H3 example @@ -95,17 +95,17 @@ export default (_app: Express): MiddlewareConfig => { `h3/src/config/middleware.ts`: ```ts -import { H3 } from 'h3'; -import { MiddlewareConfig } from 'src/types/config'; -import { cors } from '@app/http/middlewares/cors'; +import { H3 } from 'h3' +import { MiddlewareConfig } from 'src/types/config' +import { cors } from '@app/http/middlewares/cors' export default (_app: H3): MiddlewareConfig => { return { global: [cors()], before: [], after: [], - }; -}; + } +} ``` ## Route-level middleware @@ -115,11 +115,11 @@ Use config middleware for app-wide concerns. For route-specific behavior, attach Authentication middleware is provided by runtime driver packages so it can integrate with each framework naturally: ```ts -import { auth } from '@arkstack/driver-express/middlewares'; +import { auth } from '@arkstack/driver-express/middlewares' ``` ```ts -import { auth } from '@arkstack/driver-h3/middlewares'; +import { auth } from '@arkstack/driver-h3/middlewares' ``` For a full auth flow, see [Authentication](/guide/authentication). @@ -127,21 +127,19 @@ For a full auth flow, see [Authentication](/guide/authentication). Example from Express route registration: ```ts -import { Router } from '@arkstack/driver-express'; +import { Router } from '@arkstack/driver-express' Router.get( '/', ({ res }) => { - res - .setHeader('Content-Type', 'text/html') - .send('Welcome to the Express application!'); + res.setHeader('Content-Type', 'text/html').send('Welcome to the Express application!') }, [ () => { - console.log('Middleware for root route'); + console.log('Middleware for root route') }, ], -); +) ``` ## Team expectation diff --git a/docs/guide/notifications.md b/docs/guide/notifications.md index d8826e08..4bf70961 100644 --- a/docs/guide/notifications.md +++ b/docs/guide/notifications.md @@ -28,7 +28,7 @@ Notification configuration is split into a default notification driver, driver-l ```ts // src/config/notifications.ts -import { env } from '@arkstack/common'; +import { env } from '@arkstack/common' export default () => ({ default_driver: env('NOTIFICATION_DRIVER', 'mail'), @@ -67,7 +67,7 @@ export default () => ({ from: env('TWILIO_FROM'), }, }, -}); +}) ``` `default_driver` selects the notification channel used by `Notification.channel()`. `drivers.sms.transport` selects the SMS transport provider, and starter config reads that value from `SMS_TRANSPORT`. @@ -77,20 +77,17 @@ export default () => ({ Mail uses the configured SMTP transport. Recipients can be a string, an array of strings, a named address object, or an array of named address objects. ```ts -import { Notification } from '@arkstack/notifications'; +import { Notification } from '@arkstack/notifications' await Notification.mail() .recipient({ 'ada@example.com': 'Ada Lovelace' }) .subject('Welcome, {name}') .data({ name: 'Ada' }) - .send('Hello {name}, thanks for joining.'); + .send('Hello {name}, thanks for joining.') await Notification.mail() - .recipient([ - { 'grace@example.com': 'Grace Hopper' }, - { 'katherine@example.com': 'Katherine Johnson' }, - ]) - .send('The report is ready.'); + .recipient([{ 'grace@example.com': 'Grace Hopper' }, { 'katherine@example.com': 'Katherine Johnson' }]) + .send('The report is ready.') ``` ## SMS @@ -98,19 +95,17 @@ await Notification.mail() SMS supports `africastalking` and `twilio` transports. ```ts -import { Notification } from '@arkstack/notifications'; +import { Notification } from '@arkstack/notifications' -await Notification.sms() - .recipient('+2348012345678') - .send('Your login code is {code}', undefined, undefined, { - code: '123456', - }); +await Notification.sms().recipient('+2348012345678').send('Your login code is {code}', undefined, undefined, { + code: '123456', +}) await Notification.sms({ transport: 'twilio' }) .recipient('+15551234567') .send('Your login code is {code}', undefined, undefined, { code: '123456', - }); + }) ``` ## Database Notifications @@ -123,7 +118,7 @@ await Notification.db() .type('security') .action('Review login', '/account/security') .meta({ device: 'Chrome on macOS' }) - .send('A new login was detected.', 'Security alert'); + .send('A new login was detected.', 'Security alert') ``` You can also broacast the new database notifcation using the configured realtime driver by channing the `broadcast` method. @@ -135,7 +130,7 @@ await Notification.db() .type('security') .action('Review login', '/account/security') .meta({ device: 'Chrome on macOS' }) - .send('A new login was detected.', 'Security alert'); + .send('A new login was detected.', 'Security alert') ``` The notification is stored, broadcast on the user's channel, and pushed to their devices when [device push](#device-push) is configured. @@ -143,11 +138,11 @@ The notification is stored, broadcast on the user's channel, and pushed to their Use `UserNotificationCenter` when you need to list, mark, or delete stored notifications: ```ts -import { UserNotificationCenter } from '@arkstack/notifications'; +import { UserNotificationCenter } from '@arkstack/notifications' -const unread = await UserNotificationCenter.unreadForUser(user); -await UserNotificationCenter.markRead(unread[0]); -await UserNotificationCenter.delete(unread[0]); +const unread = await UserNotificationCenter.unreadForUser(user) +await UserNotificationCenter.markRead(unread[0]) +await UserNotificationCenter.delete(unread[0]) ``` ## Realtime Notifications @@ -160,7 +155,7 @@ await Notification.realtime() .type('transaction') .action('View', '/wallet') .store() // also persist so the client can load history - .send('You received $20.00', 'Payment received'); + .send('You received $20.00', 'Payment received') ``` `.store()` is opt-in: when enabled the notification is written via `UserNotificationCenter` (giving it a real id and timestamps) **and** broadcast; otherwise it is broadcast only. Broadcast on an explicit channel with `.channel('team.updates')`, or change the client event name with `.event('alert')`. @@ -169,13 +164,13 @@ await Notification.realtime() `drivers.realtime.transport` picks how events reach clients: -| Transport | Delivers to | Server SDK | Client SDK | -| ------------------- | ------------------------------------ | ---------------- | ----------- | +| Transport | Delivers to | Server SDK | Client SDK | +| ------------------- | ------------------------------------- | ---------------- | ----------- | | `pusher` | Pusher Channels, or soketi via `host` | `pusher` | `pusher-js` | -| `firebase` | FCM devices and topics | `firebase-admin` | `firebase` | -| `firebase-database` | Firebase Realtime Database channels | `firebase-admin` | `firebase` | -| `polling` | Nothing; clients poll on an interval | none | none | -| `memory` | An in-process buffer, for tests | none | none | +| `firebase` | FCM devices and topics | `firebase-admin` | `firebase` | +| `firebase-database` | Firebase Realtime Database channels | `firebase-admin` | `firebase` | +| `polling` | Nothing; clients poll on an interval | none | none | +| `memory` | An in-process buffer, for tests | none | none | The SDKs are optional peer dependencies; install only the ones you use: @@ -191,7 +186,7 @@ To use a self-hosted Pusher-protocol server such as [soketi](https://soketi.app) ```ts await Notification.realtime({ transport: 'firebase' }) .channel(user.deviceTokens) // string[] of FCM registration tokens - .send('You have a new message'); + .send('You have a new message') ``` ### Broadcasting events @@ -199,13 +194,12 @@ await Notification.realtime({ transport: 'firebase' }) A channel is just a channel. Use `Broadcast` to send your own event shapes over the configured transport, and pick them up with the client's `listen()`: ```ts -import { Broadcast } from '@arkstack/notifications'; +import { Broadcast } from '@arkstack/notifications' -await Broadcast.broadcast( - [`private-order.${order.id}`, `private-team.${order.teamId}`], - 'order.updated', - { id: order.id, status: order.status }, -); +await Broadcast.broadcast([`private-order.${order.id}`, `private-team.${order.teamId}`], 'order.updated', { + id: order.id, + status: order.status, +}) ``` ### Channel authorization @@ -214,13 +208,13 @@ Register who may join each private channel. `{name}` placeholders match one segm ```ts // src/routes/realtime.ts -import { Broadcast } from '@arkstack/notifications'; +import { Broadcast } from '@arkstack/notifications' Broadcast.channel('private-team.{teamId}', async (user, { teamId }) => { - return await TeamMember.where({ teamId, userId: user.id }).exists(); -}); + return await TeamMember.where({ teamId, userId: user.id }).exists() +}) -await Broadcast.routes(); // POST /realtime/auth, GET /realtime/config +await Broadcast.routes() // POST /realtime/auth, GET /realtime/config ``` `Broadcast.routes()` registers two routes behind the framework `auth` middleware (pass `{ prefix, middleware }` to change them): @@ -246,10 +240,10 @@ realtime: { The send result carries the multicast outcome, so prune dead tokens there: ```ts -const result = await Notification.realtime().recipient(user).send('Your payout has settled.'); +const result = await Notification.realtime().recipient(user).send('Your payout has settled.') if (result.push?.invalidTokens.length) { - await UserPushToken.whereIn('token', result.push.invalidTokens).delete(); + await UserPushToken.whereIn('token', result.push.invalidTokens).delete() } ``` @@ -303,7 +297,7 @@ await Notification.realtime({ transport: 'firebase' }) .priority('high') .ttl(60) .collapseKey(`signin:${attemptId}`) // a retry replaces the prompt it repeats - .send('Approve sign-in from Lagos?'); + .send('Approve sign-in from Lagos?') ``` Pass TTL in seconds; each platform's unit is converted for you. Set defaults under `drivers.realtime.delivery`, override per send, and reach anything unmapped through `.delivery({ android, apns, webpush })`. Pusher ignores all of it — it pushes over a connection the client already holds open. @@ -322,14 +316,14 @@ await Notification.realtime({ transport: 'firebase' }) .collapseKey(`signin:${attemptId}`) .priority('high') .ttl(60) - .send('Approve sign-in from Lagos?'); + .send('Approve sign-in from Lagos?') // answered elsewhere await Notification.realtime({ transport: 'firebase' }) .channel(tokens) .tag(`signin:${attemptId}`) .collapseKey(`signin:${attemptId}`) - .send('Sign-in approved'); + .send('Sign-in approved') ``` Tag by the thing it is about (`order:42`), not the message. `tag` supersedes what was **displayed**; `collapseKey` supersedes what is still **queued** — set both when you want both. They stay separate because FCM allows only four collapse keys per device, so deriving one per tag would starve that budget. @@ -339,9 +333,9 @@ Tag by the thing it is about (`order:42`), not the message. `tag` supersedes wha The React and Vue bindings apply this for you; for your own list, use the same reducer: ```ts -import { supersede } from '@arkstack/realtime'; +import { supersede } from '@arkstack/realtime' -client.subscribe(channel, (n) => setItems((items) => supersede(items, n, 50))); +client.subscribe(channel, (n) => setItems((items) => supersede(items, n, 50))) ``` Configure the transport in `src/config/notifications.ts` (`drivers.realtime`) and its credentials under `transports.*`. @@ -349,7 +343,7 @@ Configure the transport in `src/config/notifications.ts` (`drivers.realtime`) an **Bringing your own transport.** `driverFactory` replaces the built-ins with anything implementing `RealtimeDriver` — a backend this package does not ship, or a fake in a test. Set it per send, or under `drivers.realtime` for the whole app; it takes precedence over `transport`. Do connection setup lazily inside the driver, as the bundled ones do. ```ts -Notification.realtime({ driverFactory: () => new MyApnsDriver() }); +Notification.realtime({ driverFactory: () => new MyApnsDriver() }) ``` `broadcast()` also accepts payloads that are not notifications, so an application can push its own event shapes over the same channel and pick them up with the client's `listen()`. @@ -378,9 +372,9 @@ Everything else stays unreadable, so the grants and the event index are never ex Events are not deleted automatically. Schedule a prune; each call removes at most `limit` events: ```ts -import { Broadcast, FirebaseDatabaseRealtimeDriver } from '@arkstack/notifications'; +import { Broadcast, FirebaseDatabaseRealtimeDriver } from '@arkstack/notifications' -const driver = Broadcast.make('firebase-database') as FirebaseDatabaseRealtimeDriver; +const driver = Broadcast.make('firebase-database') as FirebaseDatabaseRealtimeDriver while ((await driver.prune(24 * 3600, 500)) > 0); ``` @@ -390,13 +384,13 @@ while ((await driver.prune(24 * 3600, 500)) > 0); Set the transport to `memory` and read what was sent. The buffer lives on `globalThis`, so broadcasts made by code loaded separately from your tests are recorded too: ```ts -import { MemoryRealtimeDriver } from '@arkstack/notifications'; +import { MemoryRealtimeDriver } from '@arkstack/notifications' -beforeEach(() => MemoryRealtimeDriver.clear()); +beforeEach(() => MemoryRealtimeDriver.clear()) expect(MemoryRealtimeDriver.broadcasts()).toContainEqual( expect.objectContaining({ channel: 'user.7', event: 'notification' }), -); +) ``` **Firebase credentials** can be provided two ways. Point `admin_sdk_path` (`FIREBASE_ADMINSDK`, default `firebase-adminsdk.json`, resolved from the project root) at a downloaded service-account JSON file; if that file is absent, the driver falls back to the inline `project_id` / `client_email` / `private_key` values (`FIREBASE_PROJECT_ID` / `FIREBASE_CLIENT_EMAIL` / `FIREBASE_PRIVATE_KEY`). `app_name` (`FIREBASE_APP_NAME`, default your `APP_NAME`) names the Firebase Admin app instance so repeated broadcasts reuse it. @@ -406,65 +400,82 @@ expect(MemoryRealtimeDriver.broadcasts()).toContainEqual( Install `@arkstack/realtime` in your front-end and the matching client SDK (`pusher-js` or `firebase`). The simplest setup creates the client from the server's config route and adds your API's auth header: ```ts -import { createRealtime } from '@arkstack/realtime'; +import { createRealtime } from '@arkstack/realtime' -const headers = { Authorization: `Bearer ${token}` }; -const config = await fetch('/api/realtime/config', { headers }).then((r) => r.json()); +const headers = { Authorization: `Bearer ${token}` } +const config = await fetch('/api/realtime/config', { headers }).then((r) => r.json()) const realtime = createRealtime({ ...config, - pusher: config.pusher && { ...config.pusher, apiBase: '/api', auth: { headers } }, + pusher: config.pusher && { + ...config.pusher, + apiBase: '/api', + auth: { headers }, + }, 'firebase-database': config['firebase-database'] && { ...config['firebase-database'], apiBase: '/api', auth: { headers }, }, -}); +}) const unsubscribe = await realtime.forUser(user.id, (notification) => { - console.log(notification.title, notification.description); -}); + console.log(notification.title, notification.description) +}) // Your own events -await realtime.listen(`private-order.${order.id}`, 'order.updated', refresh); +await realtime.listen(`private-order.${order.id}`, 'order.updated', refresh) ``` Under `polling` nothing arrives on a channel. `onPoll()` runs a handler on the advertised interval only while polling, so one code path serves every transport: ```ts -await realtime.listen(channel, 'order.updated', refresh); -await realtime.onPoll(refresh); +await realtime.listen(channel, 'order.updated', refresh) +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..5722af5c 100644 --- a/templates/express-inertia/tsconfig.json +++ b/templates/express-inertia/tsconfig.json @@ -1,12 +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/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/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/**'], - } - } + }, })