Pular para o conteúdo

API de Adaptadores do Astro

Astro foi projetado para ser fácil realizar deploy em qualquer provedor da nuvem para SSR (renderização no lado do servidor). Essa habilidade é providenciada por adaptadores, que são integrações. Veja o guia de SSR para aprender como utilizar um adaptador existente.

Um adaptador é um tipo especial de integração que providencia um ponto de entrada para a renderização no lado do servidor. Um adaptador faz duas coisas:

  • Implementa APIs específicas de uma hospedagem para lidar com requisições.
  • Configura a construção final de acordo com as convenções da hospedagem.

Um adaptador é uma integração e pode fazer tudo que uma integração pode.

Um adaptador deve chamar a API setAdapter no hook astro:config:done assim:

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
supportedAstroFeatures: {
staticOutput: 'stable'
}
});
},
},
};
}

O objeto passado para setAdapter é definido como:

interface AstroAdapter {
name: string;
serverEntrypoint?: string;
exports?: string[];
adapterFeatures: AstroAdapterFeatures;
supportedAstroFeatures: AstroFeatureMap;
}
export interface AstroAdapterFeatures {
/**
* Cria uma função na edge que irá se comunicar com o middleware do Astro.
*/
edgeMiddleware: boolean;
/**
* SSR apenas. Cada rota se torna sua própria função/arquivo.
*/
functionPerRoute: boolean;
}
export type SupportsKind = 'unsupported' | 'stable' | 'experimental' | 'deprecated';
export type AstroFeatureMap = {
/**
* O adaptador é capaz de servir páginas estáticas
*/
staticOutput?: SupportsKind;
/**
* O adaptador é capaz de servir páginas que são estáticas ou renderizadas pelo servidor
*/
hybridOutput?: SupportsKind;
/**
* O adaptador é capaz de servir páginas SSR
*/
serverOutput?: SupportsKind;
/**
* O adaptador pode emitir assets estáticos
*/
assets?: AstroAssetsFeature;
};
export interface AstroAssetsFeature {
supportKind?: SupportsKind;
/**
* Define se o adaptador faz deploy de arquivos em um ambiente que é compatível com a biblioteca `sharp`
*/
isSharpCompatible?: boolean;
/**
* Define se o adaptador faz deploy de arquivos em um ambiente que é compatível com a biblioteca `squoosh`
*/
isSquooshCompatible?: boolean;
}

As propriedades são:

  • name: Um nome único para seu adaptador, usado em logs.
  • serverEntrypoint: O ponto de entrada para renderização no lado do servidor.
  • exports: Um array de exportações nomeadas quando utilizado em conjunto com createExports (explicado abaixo).
  • adapterFeatures: Um objeto que habilita funcionalidades específicas que devem ser suportadas pelo adaptador. Essas funcionalidades irão mudar o resultado da build, e o adaptador deve implementar a lógica adequada para lidar com esse resultado diferente.
  • supportedAstroFeatures: Um mapa das funcionalidades integradas do Astro. Isso permite o Astro determinar quais funcionalidades um adaptador é incapaz ou não quer suportar para que as mensagens de erro apropriadas sejam fornecidas.

A API de adaptadores do Astro tenta trabalhar com qualquer tipo de hospedagem e dá uma forma flexível de se adequar com as APIs da hospedagem.

Algumas hospedagens serverless esperam que você exporte uma função, como handler:

...
export function handler(evento, contexto) {
}

Com a API de adaptadores você alcança isso implementando createExports em seu serverEntrypoint:

import { App } from 'astro/app';
export function createExports(manifesto) {
const app = new App(manifesto);
const handler = (evento, contexto) => {
// ...
};
return { handler };
}

E então em sua integração, quando você chamar setAdapter, providencie o nome em exports:

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
exports: ['handler'],
});
},
},
};
}

Algumas hospedagens esperam que você inicie o servidor por si mesmo, por exemplo, observando uma porta. Para esses tipos de hospedagens, a API de adaptadores permite que você exporte uma função start que será chamado quando o script de construção é executado.

import { App } from 'astro/app';
export function start(manifesto) {
const app = new App(manifesto);
addEventListener('fetch', evento => {
// ...
});
}

Este módulo é utilizado para renderizar páginas que foram pré-construídas através de astro build. Astro utiliza os objetos padrões Request e Response. Hospedagens que têm uma API diferente para requisições/respostas devem ser convertidos para estes tipos em seus adaptadores.

import { App } from 'astro/app';
import http from 'http';
export function start(manifesto) {
const app = new App(manifesto);
addEventListener('fetch', evento => {
evento.respondWith(
app.render(evento.request)
);
});
}

Os métodos a seguir são fornecidos:

app.render(requisicao, { routeData, locals })
Seção intitulada app.render(requisicao, { routeData, locals })

Este método chama a página Astro que corresponde a requisição, a renderiza e retorna uma Promise a um objeto Response. Isso também funciona para rotas de API que não renderizam páginas.

const resposta = await app.render(requisicao);

O método aceita um argumento requisicao obrigatório e um argumento opcional para routeData e locals.

Forneça um valor para routeData se você já sabe a rota para renderizar. Fazendo isso vai ignorar a chamada interna para app.match para determinar a rota para renderizar.

O exemplo abaixo lê o cabeçalho chamado x-cabecalho-privado, tenta analisá-lo como um objeto e passa isso para locals, que pode então ser passado para qualquer função middleware.

const cabecalhoPrivado = request.headers.get("x-cabecalho-privado");
let locals = {};
try {
if (cabecalhoPrivado) {
locals = JSON.parse(cabecalhoPrivado);
}
} finally {
const response = await app.render(request, { locals });
}

Este método é utilizado para determinar se uma requisição é correspondida pelas regras de roteamento da aplicação Astro.

if(app.match(requisicao)) {
const resposta = await app.render(requisicao);
}

Você geralmente pode chamar app.render(requisicao) sem utilizar .match pois Astro lida com 404s se você providenciar um arquivo 404.astro. Utilize app.match(requisicao) se você quiser lidar com 404s de forma diferente.

O comando astro add permite que usuários facilmente adicionem integrações e adaptadores em seus projetos. Se você quiser que seu adaptador seja instalável com essa ferramenta, adicione astro-adapter no campo keywords do seu package.json:

{
"name": "exemplo",
"keywords": ["astro-adapter"],
}

Assim que você publicar seu adaptador no npm, executar astro add exemplo irá instalar seu pacote com quaisquer dependências de pares especificadas em seu package.json. Nós também iremos instruir usuários a atualizarem a configuração de seus projetos manualmente.

Adicionado em: astro@3.0.0

Funcionalidades do Astro é uma forma para que adaptadores digam ao Astro se eles são ou não capazes de suportar uma funcionalidade, e também o nível de suporte do adaptador.

Ao utilizar essas propriedades, Astro irá:

  • executar validação específica;
  • emitir contextos para os registros;

Essas operações são executadas com base nas funcionalidades suportadas ou não, seu nível de suporte, e a configuração que o usuário utiliza.

A configuração a seguir diz ao Astro que esse adaptador tem suporte experimental para assets, porém o adaptador não é compatível com os serviços Sharp e Squoosh integrados:

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
supportedAstroFeatures: {
assets: {
supportKind: "experimental",
isSharpCompatible: false,
isSquooshCompatible: false
}
}
});
},
},
};
}

Astro irá registar um aviso ao terminal:

[@matthewp/meu-adaptador] A funcionalidade é experimental e sujeita a problemas ou mudanças.

e um erro se o serviço usado para assets não é compatível com o adaptador:

[@matthewp/meu-adaptador] O adaptador atualmente selecionado `@matthewp/meu-adaptador` não é compatível com o serviço "Sharp". Seu projeto não será capaz de build.

Um conjunto de funcionalidades que modificam o resultado dos arquivos emitidos. Quando um adaptador opta por essas funcionalidades, ele irá adquirir informação adicional dentro de hooks específicos.

Essa é uma funcionalidade que é habilitada ao utilizar SSR apenas. Por padrão, Astro emite um único arquivo entry.mjs, que é responsável por emitir a página renderizada a cada requisição.

Quando functionPerRoute é true, Astro irá ao invés disso criar um arquivo separado para cada rota definida no projeto.

Cada arquivo emitido irá apenas renderizar uma página. As páginas serão emitidas dentro do diretório dist/pages/ (ou dentro de um diretório /pages/ no diretório especificado para outDir), e os arquivos emitidos irão manter o mesmo caminho de arquivo que o diretório src/pages/.

Os arquivos dentro do diretório pages/ da build refletirão a estrutura do diretório dos seus arquivos de página em src/pages/, por exemplo:

  • Diretóriodist/
    • Diretóriopages/
      • Diretórioblog/
        • entry._slug_.astro.mjs
        • entry.sobre.astro.mjs
      • entry.index.astro.mjs

Habilite essa funcionalidade passando true ao adaptador.

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
adapterFeatures: {
functionPerRoute: true
}
});
},
},
};
}

Então, consuma o hook astro:build:ssr, que irá te dar um objeto entryPoints que mapeia uma rota de página ao arquivo físico emitido após a build.

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
adapterFeatures: {
functionPerRoute: true
}
});
},
'astro:build:ssr': ({ entryPoints }) => {
for (const [route, entryFile] of entryPoints) {
// faça algo com a route e entryFile
}
}
},
};
}

Definir functionPerRoute: true em um ambiente serverless cria um arquivo JavaScript (handler) para cada rota. Um handler pode terr nomes diferentes com base na sua plataforma de hospedagem: lambda, function, page, etc.

Cada uma dessas rotas é sujeita a um cold start quando o handler é executado, o que pode causar algum delay. Esse delay é influenciado por diferentes fatores.

Com functionPerRoute: false, há apenas um único handler no comando da renderização de todas as suas rotas. Quando esse handler é ativado pela primeira vez, você estará sujeito a um cold start. E então, todas as outras rotas devem funcionar sem delay. Porém, você irá perder o benefício da separação de código que functionPerRoute: true fornece.

Define se algum código de middleware SSR passará por bundle ao ser construído.

Quando habilitado, isso previne código de middleware de passar por bundle e ser importado por todas as páginas durante a build:

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
adapterFeatures: {
edgeMiddleware: true
}
});
},
},
};
}

Então, consuma o hook astro:build:ssr, que irá te dar um middlewareEntryPoint, uma URL para o arquivo físico no sistema de arquivos.

meu-adaptador.mjs
export default function createIntegration() {
return {
name: '@matthewp/meu-adaptador',
hooks: {
'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/meu-adaptador',
serverEntrypoint: '@matthewp/meu-adaptador/server.js',
adapterFeatures: {
edgeMiddleware: true
}
});
},
'astro:build:ssr': ({ middlewareEntryPoint }) => {
// lembre-se de verificar se essa propriedade existe, ela será `undefined` se o adaptador não optar pela funcionalidade
if (middlewareEntryPoint) {
createEdgeMiddleware(middlewareEntryPoint)
}
}
},
};
}
function createEdgeMiddleware(middlewareEntryPoint) {
// emite um novo arquivo físico utilizando seu bundler
}