Scaffolding e codemods
Scaffolding se refere ao processo de geração de arquivos de origem a partir de modelos estáticos (também conhecidos como stubs), e codemods se referem à atualização do código-fonte TypeScript analisando o AST.
O AdonisJS usa ambos para acelerar as tarefas repetitivas de criação de novos arquivos e configuração de pacotes. Neste guia, abordaremos os blocos de construção do Scaffolding e abordaremos a API codemods que você pode usar nos comandos Ace.
Blocos de construção
Stubs
Stubs se refere aos modelos, que são usados para criar arquivos de origem em uma determinada ação. Por exemplo, o comando make:controller
usa o controller stub para criar um arquivo de controlador dentro do projeto host.
Geradores
Os geradores impõem uma convenção de nomenclatura e geram nomes de arquivo, classe ou método com base nas convenções predefinidas.
Por exemplo, os stubs do controlador usam os geradores controllerName e controllerFileName para criar um controlador.
Como os geradores são definidos como um objeto, você pode substituir os métodos existentes para ajustar as convenções. Aprenderemos mais sobre isso mais adiante neste guia.
Codemods
A API codemods vem do pacote @adonisjs/assembler e usa ts-morph por baixo dos panos.
Como @adonisjs/assembler
é uma dependência de desenvolvimento, ts-morph
não incha as dependências do seu projeto na produção. Além disso, significa que as APIs codemods não estão disponíveis na produção.
A API codemods exposta pelo AdonisJS é muito específica para realizar tarefas de alto nível, como adicionar um provedor ao arquivo .adonisrc.ts
ou registrar um middleware dentro do arquivo start/kernel.ts
. Além disso, essas APIs dependem das convenções de nomenclatura padrão, então se você fizer mudanças drásticas no seu projeto, não poderá executar codemods.
Comando Configure
O comando configure é usado para configurar um pacote AdonisJS. Por baixo dos panos, esse comando importa o arquivo de ponto de entrada principal e executa o método configure
exportado pelo pacote mencionado.
O método configure
do pacote recebe uma instância do comando Configure e, portanto, pode acessar os stubs e a API codemods diretamente da instância do comando.
Usando stubs
Na maioria das vezes, você usará stubs dentro de um comando Ace ou dentro do método configure
de um pacote que você criou. Você pode inicializar o módulo codemods em ambos os casos por meio do método createCodemods
do comando Ace.
O método codemods.makeUsingStub
cria um arquivo de origem a partir de um modelo de stub. Ele aceita os seguintes argumentos:
- A URL para a raiz do diretório onde os stubs são armazenados.
- Caminho relativo do diretório
STUBS_ROOT
para o arquivo stub (incluindo extensão). - E o objeto de dados para compartilhar com o stub.
// Inside a command
import { BaseCommand } from '@adonisjs/core/ace'
const STUBS_ROOT = new URL('./stubs', import.meta.url)
export default class MakeApiResource extends BaseCommand {
async run() {
const codemods = await this.createCodemods()
await codemods.makeUsingStub(STUBS_ROOT, 'api_resource.stub', {})
}
}
Modelos de stubs
Usamos o mecanismo de modelo Tempura para processar os stubs com dados de tempo de execução. Tempura é um mecanismo de modelo estilo handlebars superleve para JavaScript.
DICA
Como a sintaxe do Tempura é compatível com handlebars, você pode configurar seus editores de código para usar o realce de sintaxe handlebar com arquivos .stub
.
No exemplo a seguir, criamos um stub que gera uma classe JavaScript. Ele usa as chaves duplas para avaliar valores de tempo de execução.
Usando geradores
Se você executar o stub acima agora, ele falhará porque não fornecemos as propriedades de dados modelName
e modelReference
.
Recomendamos calcular essas propriedades dentro do stub usando variáveis inline. Dessa forma, o aplicativo host pode ejetar o stub e modificar as variáveis.
{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(modelName)}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}
Destino de saída
Finalmente, temos que especificar o caminho de destino do arquivo que será criado usando o stub. Mais uma vez, especificamos o caminho de destino dentro do arquivo stub, pois ele permite que o aplicativo host ejete o stub e personalize seu destino de saída.
O caminho de destino é definido usando a função exports
. A função aceita um objeto e o exporta como o estado de saída do stub. Mais tarde, a API codemods usa esse objeto para criar o arquivo no local mencionado.
{{#var entity = generators.createEntity('user')}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(modelName)}}
{{#var resourceFileName = string(modelName).snakeCase().suffix('_resource').ext('.ts').toString()}}
{{{
exports({
to: app.makePath('app/api_resources', entity.path, resourceFileName)
})
}}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}
Aceitando nome da entidade via comando
Agora, codificamos o nome da entidade como user
dentro do stub. No entanto, você deve aceitá-lo como um argumento de comando e compartilhá-lo com o stub como o estado do modelo.
import { BaseCommand, args } from '@adonisjs/core/ace'
export default class MakeApiResource extends BaseCommand {
@args.string({
description: 'The name of the resource'
})
declare name: string
async run() {
const codemods = await this.createCodemods()
await codemods.makeUsingStub(STUBS_ROOT, 'api_resource.stub', {
name: this.name,
})
}
}
{{#var entity = generators.createEntity('user')}}
{{#var entity = generators.createEntity(name)}}
{{#var modelName = generators.modelName(entity.name)}}
{{#var modelReference = string.toCamelCase(modelName)}}
{{#var resourceFileName = string(modelName).snakeCase().suffix('_resource').ext('.ts').toString()}}
{{{
exports({
to: app.makePath('app/api_resources', entity.path, resourceFileName)
})
}}}
export default class {{ modelName }}Resource {
serialize({{ modelReference }}: {{ modelName }}) {
return {{ modelReference }}.toJSON()
}
}
Variáveis globais
As seguintes variáveis globais são sempre compartilhadas com um stub.
Variável | Descrição |
---|---|
app | Referência a uma instância da classe de aplicativo. |
generators | Referência ao módulo geradores. |
randomString | Referência à função auxiliar randomString. |
string | Uma função para criar uma instância construtor de strings. Você pode usar o construtor de strings para aplicar transformações em uma string. |
flags | Os sinalizadores de linha de comando são definidos ao executar o comando ace. |
Ejetando stubs
Você pode ejetar/copiar stubs dentro de um aplicativo AdonisJS usando o comando node ace eject
. O comando eject aceita um caminho para o arquivo stub original ou seu diretório pai e copia os modelos dentro do diretório stubs
da raiz do seu projeto.
No exemplo a seguir, copiaremos o arquivo make/controller/main.stub
do pacote @adonisjs/core
.
node ace eject make/controller/main.stub
Se você abrir o arquivo stub, ele terá o seguinte conteúdo.
{{#var controllerName = generators.controllerName(entity.name)}}
{{#var controllerFileName = generators.controllerFileName(entity.name)}}
{{{
exports({
to: app.httpControllersPath(entity.path, controllerFileName)
})
}}}
// import type { HttpContext } from '@adonisjs/core/http'
export default class {{ controllerName }} {
}
- Módulo generators para gerar o nome da classe do controlador e o nome do arquivo do controlador.
- Define o caminho de destinocustomizing-the-destination-path) para o arquivo do controlador usando a função
exports
. - Finalmente, definimos o conteúdo do controlador de scaffold.
Sinta-se à vontade para modificar o stub. Da próxima vez, as alterações serão selecionadas quando você executar o comando make:controller
.
Ejetando diretórios
Você pode ejetar um diretório inteiro de stubs usando o comando eject
. Passe o caminho para o diretório, e o comando copiará o diretório inteiro.
# Publicar todos os stubs de make
node ace eject make
# Publicar todos os stubs de make:controller
node ace eject make/controller
Usando sinalizadores CLI para personalizar o destino de saída do stub
Todos os comandos de scaffold compartilham os sinalizadores CLI (incluindo os não suportados) com os modelos de stub. Portanto, você pode usá-los para criar fluxos de trabalho personalizados ou alterar o destino de saída.
No exemplo a seguir, usamos o sinalizador --feature
para criar um controlador dentro do diretório de recursos mencionado.
node ace make:controller invoice --feature=billing
// Controller stub
{{#var controllerName = generators.controllerName(entity.name)}}
{{#var featureDirectoryName = generators.makePath('features', flags.feature)}}
{{#var controllerFileName = generators.controllerFileName(entity.name)}}
{{{
exports({
to: app.httpControllersPath(entity.path, controllerFileName)
to: app.makePath(featureDirectoryName, entity.path, controllerFileName)
})
}}}
// import type { HttpContext } from '@adonisjs/core/http'
export default class {{ controllerName }} {
}
Ejetando stubs de outros pacotes
Por padrão, o comando eject
copia modelos do pacote @adonisjs/core
. No entanto, você pode copiar stubs de outros pacotes usando o sinalizador --pkg
.
node ace eject make/migration/main.stub --pkg=@adonisjs/lucid
Como você encontra quais stubs copiar?
Você pode encontrar os stubs de um pacote visitando seu repositório GitHub. Armazenamos todos os stubs no nível raiz do pacote dentro do diretório stubs
.
Fluxo de execução de stubs
Aqui está uma representação visual de como encontramos e executamos stubs por meio do método makeUsingStub
.
API Codemods
A API codemods é alimentada por ts-morph e só está disponível durante o desenvolvimento. Você pode instanciar preguiçosamente o módulo codemods usando o método command.createCodemods
. O método createCodemods
retorna uma instância da classe Codemods.
import type Configure from '@adonisjs/core/commands/configure'
export async function configure(command: ConfigureCommand) {
const codemods = await command.createCodemods()
}
defineEnvValidations
Defina regras de validação para variáveis de ambiente. O método aceita um par de variáveis chave-valor. key
é o nome da variável env e value
é a expressão de validação como uma string.
NOTA
Este codemod espera que o arquivo start/env.ts
exista e deve ter a chamada de método export default await Env.create
.
Além disso, o codemod não substitui a regra de validação existente para uma determinada variável de ambiente. Isso é feito para respeitar as modificações no aplicativo.
const codemods = await command.createCodemods()
try {
await codemods.defineEnvValidations({
leadingComment: 'App environment variables',
variables: {
PORT: 'Env.schema.number()',
HOST: 'Env.schema.string()',
}
})
} catch (error) {
console.error('Unable to define env validations')
console.error(error)
}
// Saída
import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {
/**
* Variáveis de ambiente do aplicativo
*/
PORT: Env.schema.number(),
HOST: Env.schema.string(),
})
defineEnvVariables
Adicione uma ou várias novas variáveis de ambiente aos arquivos .env
e .env.example
. O método aceita um par de variáveis chave-valor.
const codemods = await command.createCodemods()
try {
await codemods.defineEnvVariables({
MY_NEW_VARIABLE: 'some-value',
MY_OTHER_VARIABLE: 'other-value'
})
} catch (error) {
console.error('Unable to define env variables')
console.error(error)
}
Às vezes, você pode querer não inserir o valor da variável no arquivo .env.example
. Você pode fazer isso usando a opção omitFromExample
.
const codemods = await command.createCodemods()
await codemods.defineEnvVariables({
MY_NEW_VARIABLE: 'SOME_VALUE',
}, {
omitFromExample: ['MY_NEW_VARIABLE']
})
O código acima irá inserir MY_NEW_VARIABLE=SOME_VALUE
no arquivo .env
e MY_NEW_VARIABLE=
no arquivo .env.example
.
registerMiddleware
Registre o middleware AdonisJS em uma das pilhas de middleware conhecidas. O método aceita a pilha de middleware e uma matriz de middleware para registrar.
A pilha de middleware pode ser uma de server | router | named
.
NOTA
Este codemod espera que o arquivo start/kernel.ts
exista e deve ter uma chamada de função para a pilha de middleware para a qual você está tentando registrar um middleware.
const codemods = await command.createCodemods()
try {
await codemods.registerMiddleware('router', [
{
path: '@adonisjs/core/bodyparser_middleware'
}
])
} catch (error) {
console.error('Unable to register middleware')
console.error(error)
}
// Saída
import router from '@adonisjs/core/services/router'
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])
Você pode definir o middleware nomeado da seguinte forma.
const codemods = await command.createCodemods()
try {
await codemods.registerMiddleware('named', [
{
name: 'auth',
path: '@adonisjs/auth/auth_middleware'
}
])
} catch (error) {
console.error('Unable to register middleware')
console.error(error)
}
updateRcFile
Registre providers
, commands
, defina metaFiles
e commandAliases
no arquivo adonisrc.ts
.
NOTA
Este codemod espera que o arquivo adonisrc.ts
exista e deve ter uma chamada de função export default defineConfig
.
const codemods = await command.createCodemods()
try {
await codemods.updateRcFile((rcFile) => {
rcFile
.addProvider('@adonisjs/lucid/db_provider')
.addCommand('@adonisjs/lucid/commands'),
.setCommandAlias('migrate', 'migration:run')
})
} catch (error) {
console.error('Unable to update adonisrc.ts file')
console.error(error)
}
// Saída:
import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({
commands: [
() => import('@adonisjs/lucid/commands')
],
providers: [
() => import('@adonisjs/lucid/db_provider')
],
commandAliases: {
migrate: 'migration:run'
}
})
registerJapaPlugin
Registre um plugin Japa no arquivo tests/bootstrap.ts
.
NOTA
Este codemod espera que o arquivo tests/bootstrap.ts
exista e deve ter a exportação export const plugins: Config['plugins']
.
const codemods = await command.createCodemods()
const imports = [
{
isNamed: false,
module: '@adonisjs/core/services/app',
identifier: 'app'
},
{
isNamed: true,
module: '@adonisjs/session/plugins/api_client',
identifier: 'sessionApiClient'
}
]
const pluginUsage = 'sessionApiClient(app)'
try {
await codemods.registerJapaPlugin(pluginUsage, imports)
} catch (error) {
console.error('Unable to register japa plugin')
console.error(error)
}
// Saída:
import app from '@adonisjs/core/services/app'
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
export const plugins: Config['plugins'] = [
sessionApiClient(app)
]
registerPolicies
Registre as políticas do bouncer do AdonisJS na lista de objetos policies
exportados do arquivo app/policies/main.ts
.
NOTA
Este codemod espera que o arquivo app/policies/main.ts
exista e deve exportar um objeto policies
dele.
const codemods = await command.createCodemods()
try {
await codemods.registerPolicies([
{
name: 'PostPolicy',
path: '#policies/post_policy'
}
])
} catch (error) {
console.error('Unable to register policy')
console.error(error)
}
// Saída:
export const policies = {
PostPolicy: () => import('#policies/post_policy')
}
registerVitePlugin
Registre um plugin Vite no arquivo vite.config.ts
.
NOTA
Este codemod espera que o arquivo vite.config.ts
exista e deve ter a chamada de função export default defineConfig
.
const transformer = new CodeTransformer(appRoot)
const imports = [
{
isNamed: false,
module: '@vitejs/plugin-vue',
identifier: 'vue'
},
]
const pluginUsage = 'vue({ jsx: true })'
try {
await transformer.addVitePlugin(pluginUsage, imports)
} catch (error) {
console.error('Unable to register vite plugin')
console.error(error)
}
// Saída:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({ jsx: true })
]
})
installPackages
Instale um ou vários pacotes usando o gerenciador de pacotes detectado no projeto do usuário.
const codemods = await command.createCodemods()
try {
await codemods.installPackages([
{ name: 'vinejs', isDevDependency: false },
{ name: 'edge', isDevDependency: false }
])
} catch (error) {
console.error('Unable to install packages')
console.error(error)
}
getTsMorphProject
O método getTsMorphProject
retorna uma instância de ts-morph
. Isso pode ser útil quando você deseja executar transformações de arquivo personalizadas que não são cobertas pela API Codemods.
const project = await codemods.getTsMorphProject()
project.getSourceFileOrThrow('start/routes.ts')
Certifique-se de ler a documentação do ts-morph para saber mais sobre as APIs disponíveis.