From 5a4dea0cec9180f03c010f0239ede32ae4c07696 Mon Sep 17 00:00:00 2001 From: Tom Gobich Date: Fri, 25 Oct 2024 07:52:06 -0400 Subject: [PATCH] feat(deps): improving dependency flow, provider checks, and migration existance checks --- src/scaffolds/base_scaffold.ts | 25 ++- src/scaffolds/jumpstart_scaffold.ts | 177 +++++++++++++----- src/scaffolds/tailwind_scaffold.ts | 45 +++-- src/utils/child_process.ts | 4 - stubs/controllers/auth/login_controller.stub | 2 +- .../controllers/auth/register_controller.stub | 2 +- stubs/routes/auth.stub | 29 --- stubs/routes/web.stub | 25 --- stubs/views/components/layout/index.edge | 5 +- stubs/views/pages/jumpstart.edge | 7 + stubs/views/pages/welcome.edge | 7 - 11 files changed, 185 insertions(+), 143 deletions(-) delete mode 100644 src/utils/child_process.ts delete mode 100644 stubs/routes/auth.stub delete mode 100644 stubs/routes/web.stub create mode 100644 stubs/views/pages/jumpstart.edge delete mode 100644 stubs/views/pages/welcome.edge diff --git a/src/scaffolds/base_scaffold.ts b/src/scaffolds/base_scaffold.ts index 3d2c2e5..6df54b6 100644 --- a/src/scaffolds/base_scaffold.ts +++ b/src/scaffolds/base_scaffold.ts @@ -3,7 +3,7 @@ import ConfigureCommand from '@adonisjs/core/commands/configure' import { readFileOrDefault } from '../utils/file_helper.js' import { slash } from '@adonisjs/core/helpers' import { stubsRoot } from '../../stubs/main.js' -import { cp } from 'node:fs/promises' +import { cp, readdir } from 'node:fs/promises' import { SourceFile, VariableDeclarationStructure, @@ -15,6 +15,7 @@ export default class BaseScaffold { declare codemods: Codemods #contents: Map = new Map() + #migrations?: string[] constructor(protected command: ConfigureCommand) {} @@ -34,7 +35,7 @@ export default class BaseScaffold { this.codemods = await this.command.createCodemods() } - async isProviderRegistered(path: string) { + async hasProvider(path: string) { let contents = this.#contents.get('adonisrc.ts') if (!contents) { @@ -47,7 +48,7 @@ export default class BaseScaffold { async copyView(stubName: string) { const stub = this.app.makePath(stubsRoot, 'views', stubName) - const dest = this.app.viewsPath(stubName.replace('.stub', '.ts')) + const dest = this.app.viewsPath(stubName.replace('.stub', '.edge')) await this.copyStub(stub, dest) } @@ -78,6 +79,24 @@ export default class BaseScaffold { } } + async stubMigration(migrationStub: string) { + const name = slash(migrationStub).split('.stub').at(0)?.split('/').reverse().at(0) + + if (!name) { + throw new Error(`Migration name could note be found for: ${migrationStub}`) + } + + if (!this.#migrations) { + this.#migrations = await readdir(this.app.migrationsPath()) + } + + if (this.#migrations.some((migration) => migration.includes(name))) { + return this.logger.action(`create ${name}`).skipped('migration already exists') + } + + await this.codemods.makeUsingStub(stubsRoot, migrationStub, {}) + } + getLogPath(path: string) { return slash(this.app.relativePath(path)) } diff --git a/src/scaffolds/jumpstart_scaffold.ts b/src/scaffolds/jumpstart_scaffold.ts index 99167ba..9001bab 100644 --- a/src/scaffolds/jumpstart_scaffold.ts +++ b/src/scaffolds/jumpstart_scaffold.ts @@ -1,7 +1,7 @@ -import { cp, readFile, writeFile } from 'node:fs/promises' -import BaseScaffold from './base_scaffold.js' import ConfigureCommand from '@adonisjs/core/commands/configure' +import { readFile, writeFile } from 'node:fs/promises' import { stubsRoot } from '../../stubs/main.js' +import BaseScaffold from './base_scaffold.js' import TailwindScaffold from './tailwind_scaffold.js' type Import = { @@ -11,60 +11,137 @@ type Import = { } export default class JumpstartScaffold extends BaseScaffold { + #isWeb = true + constructor(protected command: ConfigureCommand) { super(command) } static installs: { name: string; isDevDependency: boolean }[] = [ + ...TailwindScaffold.installs, { name: 'edge-iconify', isDevDependency: false }, { name: '@iconify-json/ph', isDevDependency: false }, { name: '@iconify-json/svg-spinners', isDevDependency: false }, ] async run() { + // have user install & configure required missing core packages + await this.#verifyCoreDependencies() + + // once ace commands are done, let's get our codemods set up + await this.boot() + + // first install packages, we'll install these into the user's project so they can + // update them as they see fit + const packageNames = JumpstartScaffold.installs.map((install) => install.name).join(', ') + this.logger.info(`We're going to install ${packageNames} to add TailwindCSS & Iconify`) + await this.codemods.installPackages(JumpstartScaffold.installs) + + // complete tailwindcss scaffolding + await new TailwindScaffold(this.command).run() + + // complete jumpstart scaffolding + await this.#updateEnv() + await this.#enableHttpMethodSpoofing() + await this.#registerPreloads() + await this.#generateStubs() + await this.#updateRoutes() + await this.#updateUserModel() + + this.logger.success('Jumpstart is all set! Visit /jumpstart to get started.') + } + + async #verifyCoreDependencies() { const ace = await this.app.container.make('ace') - const isAuthConfigured = await this.isProviderRegistered('@adonisjs/auth/auth_provider') - const isLucidConfigured = await this.isProviderRegistered('@adonisjs/lucid/database_provider') - const isMailConfigured = await this.isProviderRegistered('@adonisjs/mail/mail_provider') - if (!isLucidConfigured) { + const isViteConfigured = await this.hasProvider('@adonisjs/vite/vite_provider') + const isVineConfigured = await this.hasProvider('@adonisjs/core/providers/vinejs_provider') + const isEdgeConfigured = await this.hasProvider('@adonisjs/core/providers/edge_provider') + const isSessionConfigured = await this.hasProvider('@adonisjs/session/session_provider') + const isShieldConfigured = await this.hasProvider('@adonisjs/shield/shield_provider') + const isAuthConfigured = await this.hasProvider('@adonisjs/auth/auth_provider') + const isLucidConfigured = await this.hasProvider('@adonisjs/lucid/database_provider') + const isMailConfigured = await this.hasProvider('@adonisjs/mail/mail_provider') + + if (!isViteConfigured) { + this.logger.log('') // let's add a blank line in-between these this.logger.log( - this.colors.blue("You'll need @adonisjs/lucid installed and configured to continue") + this.colors.bgBlue("Vite is needed to bundle tailwind assets, let's add @adonisjs/vite") ) - await ace.exec('add', ['@adonisjs/lucid']) + + await ace.exec('add', ['@adonisjs/vite']) } - if (!isAuthConfigured) { + if (!isVineConfigured) { + this.logger.log('') // let's add a blank line in-between these this.logger.log( - this.colors.blue("You'll need @adonisjs/auth installed and configured to continue") + this.colors.bgBlue("VineJS is needed for Jumpstart's validations, let's add vinejs") ) - await ace.exec('add', ['@adonisjs/auth']) + + await ace.exec('add', ['vinejs']) } - if (!isMailConfigured) { + if (!isEdgeConfigured) { + this.logger.log('') // let's add a blank line in-between these this.logger.log( - this.colors.blue("You'll need @adonisjs/mail installed and configured to continue") + this.colors.bgBlue("Jumpstart uses EdgeJS for its pages & emails, let's add edge") ) - await ace.exec('add', ['@adonisjs/mail']) + + await ace.exec('add', ['edge']) } - await this.boot() + if (!isSessionConfigured) { + this.logger.log('') // let's add a blank line in-between these + this.logger.log( + this.colors.bgBlue( + "Session is needed for authentication & toast messaging, let's add @adonisjs/session" + ) + ) - await this.codemods.installPackages([ - ...TailwindScaffold.installs, - ...JumpstartScaffold.installs, - ]) + await ace.exec('add', ['@adonisjs/session']) + } - await new TailwindScaffold(this.command).run() + if (!isShieldConfigured) { + this.logger.log('') // let's add a blank line in-between these + this.logger.log( + this.colors.bgBlue( + "Shield is recommended for CSRF & other protections, let's add @adonisjs/shield" + ) + ) - await this.#updateEnv() - await this.#enableHttpMethodSpoofing() - await this.#registerPreloads() - await this.#generateStubs() - await this.#updateRoutes() - await this.#updateUserModel() + await ace.exec('add', ['@adonisjs/shield']) + } + + if (!isLucidConfigured) { + this.logger.log('') // let's add a blank line in-between these + this.logger.log( + this.colors.bgBlue( + "Jumpstart uses Lucid as it's ORM for models & queries, let's add @adonisjs/lucid" + ) + ) + + await ace.exec('add', ['@adonisjs/lucid']) + } + + if (!isAuthConfigured) { + this.logger.log('') // let's add a blank line in-between these + this.logger.log( + this.colors.bgBlue("Jumpstart adds authentication scaffolding, let's add @adonisjs/auth") + ) - this.logger.success('Jumpstart is all set! Visit /welcome to get started.') + await ace.exec('add', ['@adonisjs/auth', '--guard=session']) + } + + if (!isMailConfigured) { + this.logger.log('') // let's add a blank line in-between these + this.logger.log( + this.colors.bgBlue( + "Jumpstart includes emails for the forgot password flow & email change notifications. Let's add @adonisjs/mail" + ) + ) + + await ace.exec('add', ['@adonisjs/mail']) + } } async #updateEnv() { @@ -78,6 +155,8 @@ export default class JumpstartScaffold extends BaseScaffold { } async #enableHttpMethodSpoofing() { + if (!this.#isWeb) return + const appConfigPath = this.app.makePath('config/app.ts') let appConfig = await readFile(appConfigPath, 'utf8') @@ -89,6 +168,8 @@ export default class JumpstartScaffold extends BaseScaffold { } async #registerPreloads() { + if (!this.#isWeb) return + await this.codemods.makeUsingStub(stubsRoot, 'start/globals.stub', {}) await this.codemods.updateRcFile((rcFile) => { rcFile.addPreloadFile('#start/globals') @@ -98,28 +179,18 @@ export default class JumpstartScaffold extends BaseScaffold { async #generateStubs() { //* NOTE: copy utils from base_scaffold exist because Tempura throws an exception on the backticked contents (escaped or not) - // stubs -> views -- using cp due to the number of files - await cp(this.app.makePath(stubsRoot, 'views'), this.app.viewsPath(), { - recursive: true, - force: false, - }) + // stubs -> views + if (this.#isWeb) { + await this.copyView('components') + await this.copyView('pages') + } - this.logger - .action(`copy ${this.getLogPath(this.app.viewsPath())} -> pages, emails, components`) - .succeeded() + await this.copyView('emails') // stubs -> migrations - await this.codemods.makeUsingStub(stubsRoot, 'migrations/create_email_histories_table.stub', {}) - await this.codemods.makeUsingStub( - stubsRoot, - 'migrations/create_password_reset_tokens_table.stub', - {} - ) - await this.codemods.makeUsingStub( - stubsRoot, - 'migrations/create_remember_me_tokens_table.stub', - {} - ) + this.stubMigration('migrations/create_email_histories_table.stub') + this.stubMigration('migrations/create_password_reset_tokens_table.stub') + this.stubMigration('migrations/create_remember_me_tokens_table.stub') // stubs -> models await this.copyModel('email_history.stub') @@ -130,7 +201,9 @@ export default class JumpstartScaffold extends BaseScaffold { await this.codemods.makeUsingStub(stubsRoot, 'validators/settings.stub', {}) // stubs -> services - await this.codemods.makeUsingStub(stubsRoot, 'services/edge_form_service.stub', {}) + if (this.#isWeb) { + await this.codemods.makeUsingStub(stubsRoot, 'services/edge_form_service.stub', {}) + } // stubs -> controllers await this.copyController('auth/forgot_password_controller.stub') @@ -157,7 +230,7 @@ export default class JumpstartScaffold extends BaseScaffold { const contents = file.getText() const lastImportIndex = file.getImportDeclarations().reverse().at(0)?.getChildIndex() ?? 0 - console.log({ lastImportIndex }) + file.insertVariableStatements( lastImportIndex + 1, [ @@ -194,7 +267,7 @@ export default class JumpstartScaffold extends BaseScaffold { file.addStatements( [ '\n', - "router.on('/welcome').render('pages/welcome').as('welcome')", + "router.on('/jumpstart').render('pages/jumpstart').as('jumpstart')", '\n', '//* AUTH -> LOGIN, REGISTER, LOGOUT', "router.get('/login', [LoginController, 'show']).as('auth.login.show').use(middleware.guest())", @@ -276,7 +349,7 @@ export default class JumpstartScaffold extends BaseScaffold { login.setBodyText(` const user = await this.verifyCredentials(email, password) await auth.use('web').login(user, remember) - return user + return user `) } @@ -294,7 +367,7 @@ export default class JumpstartScaffold extends BaseScaffold { register.setBodyText(` const user = await this.create(data) await auth.use('web').login(user) - return user + return user `) } @@ -341,7 +414,7 @@ export default class JumpstartScaffold extends BaseScaffold { .to(emailOld) .subject(\`Your \${app.appName} email has been successfully changed\`) .htmlView('emails/account/email_changed', { user: this }) - }) + }) `) } diff --git a/src/scaffolds/tailwind_scaffold.ts b/src/scaffolds/tailwind_scaffold.ts index 894a84d..c82110a 100644 --- a/src/scaffolds/tailwind_scaffold.ts +++ b/src/scaffolds/tailwind_scaffold.ts @@ -1,5 +1,5 @@ import ConfigureCommand from '@adonisjs/core/commands/configure' -import { writeFile } from 'node:fs/promises' +import { mkdir, writeFile } from 'node:fs/promises' import { SourceFile, Symbol } from 'ts-morph' import { SyntaxKind } from 'typescript' import { stubsRoot } from '../../stubs/main.js' @@ -12,15 +12,22 @@ type Import = { } export default class TailwindScaffold extends BaseScaffold { - tailwindImport: Import = { - name: 'tailwind', - module: 'tailwindcss', - } - - autoprefixerImport: Import = { - name: 'autoprefixer', - module: 'autoprefixer', - } + #imports = new Map([ + [ + 'tailwind', + { + name: 'tailwind', + module: 'tailwindcss', + }, + ], + [ + 'autoprefixer', + { + name: 'autoprefixer', + module: 'autoprefixer', + }, + ], + ]) constructor(protected command: ConfigureCommand) { super(command) @@ -34,12 +41,13 @@ export default class TailwindScaffold extends BaseScaffold { async run() { await this.boot() - const cssPath = this.app.makePath('resources/css/app.css') + const cssPath = this.app.makePath('resources/css') + const cssFile = this.app.makePath('resources/css/app.css') const cssContents = '@tailwind base;\n@tailwind components;\n@tailwind utilities;\n' await this.codemods.makeUsingStub(stubsRoot, 'configs/tailwind.config.stub', {}) - let css = await readFileOrDefault(cssPath, '') + let css = await readFileOrDefault(cssFile, '') let wasChanged = false if (!css.includes('[x-cloak]')) { @@ -54,7 +62,8 @@ export default class TailwindScaffold extends BaseScaffold { } if (wasChanged) { - await writeFile(cssPath, css) + await mkdir(cssPath, { recursive: true }) + await writeFile(cssFile, css) this.logger.action('update resources/css/app.css') } @@ -105,7 +114,7 @@ export default class TailwindScaffold extends BaseScaffold { name: 'css', initializer: `{ postcss: { plugins: [tailwind(), autoprefixer()] } }`, }) - return [this.tailwindImport, this.autoprefixerImport] + return [...this.#imports.values()] } // 2. if there is a `css` property but not a `postcss` property, @@ -119,7 +128,7 @@ export default class TailwindScaffold extends BaseScaffold { name: 'postcss', initializer: '{ plugins: [tailwind(), autoprefixer()] }', }) - return [this.tailwindImport, this.autoprefixerImport] + return [...this.#imports.values()] } // 3. if there is a `css.postcss` property, but it doesn't contain `plugins`, @@ -133,7 +142,7 @@ export default class TailwindScaffold extends BaseScaffold { name: 'plugins', initializer: '[tailwind(), autoprefixer()]', }) - return [this.tailwindImport, this.autoprefixerImport] + return [...this.#imports.values()] } // 4. if there is a `css.postcss.plugins` property, @@ -147,12 +156,12 @@ export default class TailwindScaffold extends BaseScaffold { if (!pluginItems?.includes('tailwind()')) { plugins.insertElement(0, 'tailwind()') - imports.push(this.tailwindImport) + imports.push(this.#imports.get('tailwind')!) } if (!pluginItems.includes('autoprefixer()')) { plugins.addElement('autoprefixer()') - imports.push(this.autoprefixerImport) + imports.push(this.#imports.get('autoprefixer')!) } return imports diff --git a/src/utils/child_process.ts b/src/utils/child_process.ts deleted file mode 100644 index 243676f..0000000 --- a/src/utils/child_process.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { promisify } from 'node:util' -import child_process from 'node:child_process' - -export const exec = promisify(child_process.exec) diff --git a/stubs/controllers/auth/login_controller.stub b/stubs/controllers/auth/login_controller.stub index fb0999a..6a37ebf 100644 --- a/stubs/controllers/auth/login_controller.stub +++ b/stubs/controllers/auth/login_controller.stub @@ -14,6 +14,6 @@ export default class LoginController { session.flash('success', user.fullName ? `${baseMessage}, ${user.fullName}` : baseMessage) - return response.redirect().toRoute('welcome') + return response.redirect().toRoute('jumpstart') } } diff --git a/stubs/controllers/auth/register_controller.stub b/stubs/controllers/auth/register_controller.stub index ef7b4ce..a591b20 100644 --- a/stubs/controllers/auth/register_controller.stub +++ b/stubs/controllers/auth/register_controller.stub @@ -15,6 +15,6 @@ export default class RegisterController { session.flash('success', user.fullName ? `${baseMessage}, ${user.fullName}` : baseMessage) - return response.redirect().toRoute('welcome') + return response.redirect().toRoute('jumpstart') } } diff --git a/stubs/routes/auth.stub b/stubs/routes/auth.stub deleted file mode 100644 index b28e62f..0000000 --- a/stubs/routes/auth.stub +++ /dev/null @@ -1,29 +0,0 @@ -import { middleware } from '#start/kernel' -import router from '@adonisjs/core/services/router' -const LoginController = () => import('#controllers/auth/login_controller') -const LogoutController = () => import('#controllers/auth/logout_controller') -const RegisterController = () => import('#controllers/auth/register_controller') -const ForgotPasswordsController = () => import('#controllers/auth/forgot_passwords_controller') - -// ignore formatting, easier to visually scan single-line routes -/* prettier-ignore-start */ -/* eslint-disable */ - -//* LOGIN, REGISTER, LOGOUT -router.get('/login', [LoginController, 'show']).as('auth.login.show').use(middleware.guest()) -router.post('/login', [LoginController, 'store']).as('auth.login.store').use([middleware.guest()]) -router.get('/register', [RegisterController, 'show']).as('auth.register.show').use(middleware.guest()) -router.post('/register', [RegisterController, 'store']).as('auth.register.store').use([middleware.guest()]) -router.post('/logout', [LogoutController, 'handle']).as('auth.logout').use(middleware.auth()) - -//* FORGOT PASSWORD -router.get('/forgot-password', [ForgotPasswordsController, 'index']).as('auth.password.index').use([middleware.guest()]) -router.post('/forgot-password', [ForgotPasswordsController, 'send']).as('auth.password.send').use([middleware.guest()]) -router.get('/forgot-password/reset/:value', [ForgotPasswordsController, 'reset']).as('auth.password.reset').use([middleware.guest()]) -router.post('/forgot-password/reset', [ForgotPasswordsController, 'update']).as('auth.password.update').use([middleware.guest()]) - -{{{ - exports({ - to: app.makePath(app.startPath(), 'routes/auth.ts'), - }) -}}} diff --git a/stubs/routes/web.stub b/stubs/routes/web.stub deleted file mode 100644 index b5c0a66..0000000 --- a/stubs/routes/web.stub +++ /dev/null @@ -1,25 +0,0 @@ -import { middleware } from '#start/kernel' -import router from '@adonisjs/core/services/router' -const ProfileController = () => import('#controllers/settings/profile_controller') -const AccountController = () => import('#controllers/settings/account_controller') - -// ignore formatting, easier to visually scan single-line routes -/* prettier-ignore-start */ -/* eslint-disable */ - -router.on('/welcome').render('pages/welcome').as('welcome') - -//* SETTINGS -> ACCOUNT -router.get('/settings/account', [AccountController, 'index']).as('settings.account').use(middleware.auth()) -router.put('/settings/account/email', [AccountController, 'updateEmail']).as('settings.account.email').use(middleware.auth()) -router.delete('/settings/account', [AccountController, 'destroy']).as('settings.account.destroy').use(middleware.auth()) - -//* SETTINGS -> PROFILE -router.get('/settings/profile', [ProfileController, 'index']).as('settings.profile').use(middleware.auth()) -router.put('/settings/profile', [ProfileController, 'update']).as('settings.profile.update').use(middleware.auth()) - -{{{ - exports({ - to: app.makePath(app.startPath(), 'routes/web.ts'), - }) -}}} diff --git a/stubs/views/components/layout/index.edge b/stubs/views/components/layout/index.edge index c314204..d561772 100644 --- a/stubs/views/components/layout/index.edge +++ b/stubs/views/components/layout/index.edge @@ -19,12 +19,11 @@
- + {{ app.appName }}