Jogo de Dungeons com IA
Módulo 1: Configuração do monorepositório
Vamos começar criando um novo monorepositório. Dentro do diretório desejado, execute o seguinte comando:
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --ci=skip
npx create-nx-workspace@~21.0.3 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --ci=skip
Isso configurará um monorepositório NX dentro do diretório dungeon-adventure
que você pode abrir no VSCode. Deve parecer com o seguinte:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ aqui é onde seus subprojetos residirão
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configura o CLI do NX e padrões do monorepositório
- package.json todas as dependências Node são definidas aqui
- pnpm-lock.yaml ou bun.lock, yarn.lock, package-lock.json dependendo do gerenciador de pacotes
- pnpm-workspace.yaml se usar pnpm
- README.md
- tsconfig.base.json todos os subprojetos Node estendem este
- tsconfig.json
Agora estamos prontos para começar a criar nossos diferentes subprojetos usando o @aws/nx-plugin
.
Game API
Primeiro vamos criar nossa Game API. Para isso, vamos criar uma API tRPC chamada GameApi
seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#trpc-api
- Preencha os parâmetros obrigatórios
- name: GameApi
- Clique em
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem em sua árvore de arquivos.
Arquivos atualizados pelo ts#trpc-api
Abaixo está a lista de todos os arquivos gerados pelo gerador ts#trpc-api
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directoryapis/
- game-api.ts construct CDK para criar sua API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs CDK genéricos
Directoryapi/
- rest-api.ts construct base para API Gateway Rest API
- trpc-utils.ts utilitários para constructs CDK de API tRPC
- utils.ts utilitários para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directorytypes/ tipos compartilhados
Directorysrc/
- index.ts
- runtime-config.ts definição de interface usada por CDK e website
- project.json
- …
Directorygame-api/
Directorybackend/ código de implementação tRPC
Directorysrc/
Directoryclient/ cliente vanilla usado para chamadas máquina a máquina em TS
- index.ts
- sigv4.ts
Directorymiddleware/ instrumentação com Powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryprocedures/ implementações específicas dos procedimentos/rotas da API
- echo.ts
- index.ts
- init.ts configura contexto e middleware
- local-server.ts usado ao executar o servidor tRPC localmente
- router.ts ponto de entrada para o lambda handler que define todos os procedimentos
- project.json
- …
Directoryschema/
Directorysrc/
Directoryprocedures/
- echo.ts
- index.ts
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Analisando alguns dos arquivos-chave:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';
export const router = t.router;
export const appRouter = router({ echo,});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;
O router define o ponto de entrada para sua API tRPC e é onde você declarará todos os métodos da API. Como visto acima, temos um método chamado echo
com sua implementação no arquivo ./procedures/echo.ts
.
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from ':dungeon-adventure/game-api-schema';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));
Este arquivo é a implementação do método echo
e como visto é fortemente tipado através da declaração de suas estruturas de dados de entrada e saída. Ele importa essas definições do projeto :dungeon-adventure/game-api-schema
, que é um alias para o projeto de schema.
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string(),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ result: z.string(),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;
Todas as definições de schema tRPC são definidas usando Zod e exportadas como tipos TypeScript via sintaxe z.TypeOf
.
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
// Tipo union de strings para todos os nomes de operações da APItype Operations = Procedures<AppRouter>;
/** * Propriedades para criar um construct GameApi * * @template TIntegrations - Mapa de nomes de operações para suas integrações */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nomes de operações para suas integrações no API Gateway */ integrations: TIntegrations;}
/** * Um construct CDK que cria e configura uma API Gateway REST API da AWS * especificamente para GameApi. * @template TIntegrations - Mapa de nomes de operações para suas integrações */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Cria integrações padrão para todas as operações, implementando cada operação como * sua própria função lambda individual. * * @param scope - O escopo do construct CDK * @returns Um IntegrationBuilder com integrações lambda padrão */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/backend/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // Aqui concedemos a qualquer credencial AWS da conta onde o projeto é implantado para chamar a API. // Acesso refinado máquina a máquina pode ser definido aqui usando principals mais específicos (ex: roles ou // usuários) e recursos (ex: quais caminhos da API podem ser invocados por qual principal) se necessário. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Libera OPTIONS para permitir que navegadores façam requisições preflight não autenticadas new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}
Este é o construct CDK que define nossa GameApi. Como visto, ele fornece um método defaultIntegrations
que cria automaticamente uma função lambda para cada procedimento em nossa API tRPC, apontando para a implementação da API empacotada. Isso significa que no momento do cdk synth
, o empacotamento não ocorre (diferente do uso de NodeJsFunction), pois já o empacotamos como parte do target de build do projeto backend.
Story API
Agora vamos criar nossa Story API. Para isso, vamos criar uma API FastAPI chamada StoryApi
seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - py#fast-api
- Preencha os parâmetros obrigatórios
- name: StoryApi
- Clique em
Generate
pnpm nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem em sua árvore de arquivos.
Arquivos atualizados pelo py#fast-api
Abaixo está a lista de todos os arquivos gerados pelo gerador py#fast-api
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directory.venv/ único ambiente virtual para o monorepositório
- …
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directoryapis/
- story-api.ts construct CDK para criar sua API FastAPI
- index.ts atualizado para exportar o novo story-api
- project.json atualizado para adicionar dependência de build no story_api
Directorytypes/ tipos compartilhados
Directorysrc/
- runtime-config.ts atualizado para adicionar a StoryApi
Directorystory_api/
Directorystory_api/ módulo Python
- init.py configura Powertools, FastAPI e middleware
- main.py ponto de entrada para o lambda contendo todas as rotas
Directorytests/
- …
- .python-version
- project.json
- pyproject.toml
- project.json
- .python-version versão Python fixa para uv
- pyproject.toml
- uv.lock
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { OPERATION_DETAILS, Operations,} from '../../generated/story-api/metadata.gen.js';
/** * Propriedades para criar um construct StoryApi * * @template TIntegrations - Mapa de nomes de operações para suas integrações */export interface StoryApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nomes de operações para suas integrações no API Gateway */ integrations: TIntegrations;}
/** * Um construct CDK que cria e configura uma API Gateway REST API da AWS * especificamente para StoryApi. * @template TIntegrations - Mapa de nomes de operações para suas integrações */export class StoryApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Cria integrações padrão para todas as operações, implementando cada operação como * sua própria função lambda individual. * * @param scope - O escopo do construct CDK * @returns Um IntegrationBuilder com integrações lambda padrão */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: OPERATION_DETAILS, defaultIntegrationOptions: { runtime: Runtime.PYTHON_3_12, handler: 'story_api.main.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/story_api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `StoryApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: StoryApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'StoryApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // Aqui concedemos a qualquer credencial AWS da conta onde o projeto é implantado para chamar a API. // Acesso refinado máquina a máquina pode ser definido aqui usando principals mais específicos (ex: roles ou // usuários) e recursos (ex: quais caminhos da API podem ser invocados por qual principal) se necessário. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Libera OPTIONS para permitir que navegadores façam requisições preflight não autenticadas new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: OPERATION_DETAILS, ...props, }); }}
Este é o construct CDK que define nossa StoryApi. Como visto, ele fornece um método defaultIntegrations
que cria automaticamente uma função lambda para cada operação definida em nossa FastAPI, apontando para a implementação da API empacotada. Isso significa que no momento do cdk synth
, o empacotamento não ocorre (diferente de PythonFunction), pois já o empacotamos como parte do target de build do projeto backend.
export type ApiUrl = string;// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interfaceexport interface IRuntimeConfig { apis: { GameApi: ApiUrl; StoryApi: ApiUrl; };}
Aqui está um exemplo do gerador realizando uma transformação AST que preserva todo o código existente e realiza uma atualização. Observe que StoryApi
foi adicionada à definição IRuntimeConfig
, o que significa que quando isso for consumido pelo frontend, garantirá segurança de tipos!
from .init import app, lambda_handler, tracer
handler = lambda_handler
@app.get("/")@tracer.capture_methoddef read_root(): return {"Hello": "World"}
Este é onde todos os métodos da API serão definidos. Como visto aqui, temos um método read_root
mapeado para a rota GET /
. Você pode usar Pydantic para declarar entradas e saídas dos métodos e garantir segurança de tipos.
Game UI: Website
Agora vamos criar a UI que permitirá interagir com o jogo. Para isso, vamos criar um website chamado GameUI
seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#cloudscape-website
- Preencha os parâmetros obrigatórios
- name: GameUI
- Clique em
Generate
pnpm nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
yarn nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
npx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
bunx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#cloudscape-website --name=GameUI --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem em sua árvore de arquivos.
Arquivos atualizados pelo ts#cloudscape-website
Abaixo está a lista de todos os arquivos gerados pelo gerador ts#cloudscape-website
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directorystatic-websites/
- game-ui.ts construct CDK para criar sua Game UI
Directorycore/
- static-website.ts construct genérico para website estático
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.ts layout geral da página: cabeçalho, rodapé, sidebar, etc
- navitems.ts itens de navegação da sidebar
Directoryhooks/
- useAppLayout.tsx permite definir dinamicamente notificações, estilo da página, etc
Directoryroutes/ rotas baseadas em arquivo com @tanstack/react-router
- index.tsx página raiz ’/’ redireciona para ‘/welcome’
- __root.tsx todos os páginas usam este componente como base
Directorywelcome/
- index.tsx
- config.ts
- main.tsx ponto de entrada do React
- routeTree.gen.ts atualizado automaticamente pelo @tanstack/react-router
- styles.css
- index.html
- project.json
- vite.config.ts
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}
Este é o construct CDK que define nossa GameUI. Como visto, ele já configurou o caminho para o bundle gerado de nossa UI baseada em Vite. Isso significa que no momento do build
, o empacotamento ocorre dentro do target de build do projeto game-ui e sua saída é usada aqui.
import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';
import '@cloudscape-design/global-styles/index.css';
const router = createRouter({ routeTree });
// Registra a instância do router para segurança de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RouterProvider router={router} /> </I18nProvider> </React.StrictMode>, );
Este é o ponto de entrada onde o React é montado. Como mostrado, inicialmente apenas configura um @tanstack/react-router
em uma configuração de roteamento baseado em arquivo
. Isso significa que, enquanto o servidor de desenvolvimento estiver em execução, você pode simplesmente criar arquivos na pasta routes
e o @tanstack/react-router
criará a configuração boilerplate para você, atualizando o arquivo routeTree.gen.ts
. Este arquivo mantém todas as rotas de forma type-safe, o que significa que ao usar <Link>
, a opção to
só mostrará rotas válidas. Para mais informações, consulte a documentação do @tanstack/react-router
.
import { ContentLayout, Header, SpaceBetween, Container,} from '@cloudscape-design/components';import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/welcome/')({ component: RouteComponent,});
function RouteComponent() { return ( <ContentLayout header={<Header>Welcome</Header>}> <SpaceBetween size="l"> <Container>Welcome to your new Cloudscape website!</Container> </SpaceBetween> </ContentLayout> );}
Um componente que será renderizado ao navegar para a rota /welcome
. O @tanstack/react-router
gerenciará a Route
para você sempre que criar/mover este arquivo (desde que o servidor de desenvolvimento esteja em execução). Isso será mostrado em uma seção posterior deste tutorial.
Game UI: Auth
Agora vamos configurar nossa Game UI para exigir acesso autenticado via Amazon Cognito seguindo os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#cloudscape-website#auth
- Preencha os parâmetros obrigatórios
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Clique em
Generate
pnpm nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
yarn nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
npx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
bunx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#cloudscape-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem/mudarem em sua árvore de arquivos.
Arquivos atualizados pelo ts#cloudscape-website#auth
Abaixo está a lista de todos os arquivos gerados/atualizados pelo gerador ts#cloudscape-website#auth
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts construct CDK para criar pools de usuários/identidade
Directorytypes/
Directorysrc/
- runtime-config.ts atualizado para adicionar cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx adiciona usuário logado/logout ao cabeçalho
DirectoryCognitoAuth/
- index.ts gerencia login no Cognito
DirectoryRuntimeConfig/
- index.tsx busca o
runtime-config.json
e fornece aos filhos via contexto
- index.tsx busca o
Directoryhooks/
- useRuntimeConfig.tsx
- main.tsx Atualizado para adicionar Cognito
import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Registra a instância do router para segurança de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <RouterProvider router={router} /> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
Os componentes RuntimeConfigProvider
e CognitoAuth
foram adicionados ao arquivo main.tsx
via transformação AST. Isso permite que o componente CognitoAuth
autentique com o Amazon Cognito buscando o runtime-config.json
que contém a configuração de conexão necessária para fazer chamadas ao backend no destino correto.
Game UI: Conectar à Story API
Agora vamos configurar nossa Game UI para conectar à Story API criada anteriormente:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - api-connection
- Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-ui
- targetProject: dungeon_adventure.story_api
- Clique em
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=dungeon_adventure.story_api --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem/mudarem em sua árvore de arquivos.
Arquivos atualizados pela conexão UI -> FastAPI
Abaixo está a lista de todos os arquivos gerados/atualizados pelo gerador api-connection
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directoryhooks/
- useSigV4.tsx usado pela StoryApi para assinar requisições
- useStoryApiClient.tsx hook para construir um cliente StoryApi
- useStoryApi.tsx hook para interagir com a StoryApi usando TanStack Query
Directorycomponents/
- QueryClientProvider.tsx provedor do cliente TanStack Query
- StoryApiProvider.tsx Provedor para o hook TanStack Query da StoryApi
- main.tsx Instrumenta o QueryClientProvider e StoryApiProvider
- .gitignore ignora arquivos de cliente gerados
- project.json atualizado para adicionar targets de geração de hooks openapi
- …
Directorystory_api/
Directoryscripts/
- generate_open_api.py
- project.json atualizado para emitir arquivo openapi.json
import { StoryApi } from '../generated/story-api/client.gen';import { useSigV4 } from './useSigV4';import { useRuntimeConfig } from './useRuntimeConfig';import { useMemo } from 'react';
export const useStoryApi = (): StoryApi => { const runtimeConfig = useRuntimeConfig(); const apiUrl = runtimeConfig.apis.StoryApi; const sigv4Client = useSigV4(); return useMemo( () => new StoryApi({ url: apiUrl, fetch: sigv4Client, }), [apiUrl, sigv4Client], );};
Este hook pode ser usado para fazer requisições autenticadas à StoryApi
. Como visto na implementação, ele usa o StoryApi
que é gerado no momento do build, então você verá um erro em seu IDE até construirmos nosso código. Para mais detalhes sobre como o cliente é gerado ou como consumir a API, consulte o guia React para FastAPI.
import { createContext, FC, PropsWithChildren, useMemo } from 'react';import { useStoryApiClient } from '../hooks/useStoryApiClient';import { StoryApiOptionsProxy } from '../generated/story-api/options-proxy.gen';
export const StoryApiContext = createContext<StoryApiOptionsProxy | undefined>( undefined,);
export const StoryApiProvider: FC<PropsWithChildren> = ({ children }) => { const client = useStoryApiClient(); const optionsProxy = useMemo( () => new StoryApiOptionsProxy({ client }), [client], );
return ( <StoryApiContext.Provider value={optionsProxy}> {children} </StoryApiContext.Provider> );};
export default StoryApiProvider;
O componente provedor acima usa o hook useStoryApiClient
e instancia o StoryApiOptionsProxy
, que é usado para construir opções para hooks do TanStack Query. Você pode usar o hook correspondente useStoryApi
para acessar este proxy de opções, que fornece uma maneira de interagir com sua FastAPI de forma consistente com sua API tRPC.
Como useStoryApiClient
nos fornece um iterador assíncrono para nossa API de streaming, usaremos o cliente vanilla diretamente neste tutorial.
Game UI: Conectar à Game API
Agora vamos configurar nossa Game UI para conectar à Game API criada anteriormente:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - api-connection
- Preencha os parâmetros obrigatórios
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Clique em
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
npx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem/mudarem em sua árvore de arquivos.
Arquivos atualizados pela conexão UI -> tRPC
Abaixo está a lista de todos os arquivos gerados/atualizados pelo gerador api-connection
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryTrpcClients/
- index.tsx
- TrpcApis.tsx todas as APIs tRPC configuradas
- TrpcClientProviders.tsx cria um provedor de cliente por API tRPC
- TrpcProvider.tsx
Directoryhooks/
- useGameApi.tsx hooks para chamar a GameApi
- main.tsx injeta os provedores de cliente tRPC
- package.json
import { TrpcApis } from '../components/TrpcClients';
export const useGameApi = () => TrpcApis.GameApi.useTRPC();
Este hook usa a integração mais recente do React Query do tRPC, permitindo que os usuários interajam diretamente com @tanstack/react-query
sem camadas adicionais de abstração. Para exemplos de como chamar APIs tRPC, consulte o guia de uso do hook tRPC.
import TrpcClientProviders from './components/TrpcClients';import QueryClientProvider from './components/QueryClientProvider';import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Registra a instância do router para segurança de tiposdeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <TrpcClientProviders> <RouterProvider router={router} /> </TrpcClientProviders> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
O arquivo main.tsx
foi atualizado via transformação AST para injetar os provedores tRPC.
Game UI: Infraestrutura
Agora o último subprojeto que precisamos criar é para a infraestrutura CDK. Para criar isso, siga os passos abaixo:
- Instale o Nx Console VSCode Plugin se ainda não o fez
- Abra o console Nx no VSCode
- Clique em
Generate (UI)
na seção "Common Nx Commands" - Procure por
@aws/nx-plugin - ts#infra
- Preencha os parâmetros obrigatórios
- name: infra
- Clique em
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive
Você também pode realizar uma execução simulada para ver quais arquivos seriam alterados
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem/mudarem em sua árvore de arquivos.
Arquivos atualizados pelo ts#infra
Abaixo está a lista de todos os arquivos gerados/atualizados pelo gerador ts#infra
. Vamos examinar alguns dos arquivos-chave destacados na árvore de arquivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
Directorycfn-guard-rules/
- *.guard
- cfn-guard.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystacks/
- application-stack.ts recursos CDK definidos aqui
- index.ts
- main.ts ponto de entrada que define todas as stacks
- cdk.json
- project.json
- …
- package.json
- tsconfig.json adiciona referências
- tsconfig.base.json adiciona alias
import { ApplicationStack } from './stacks/application-stack.js';import { App, CfnGuardValidator, RuleSet,} from ':dungeon-adventure/common-constructs';
const app = new App({ policyValidationBeta1: [new CfnGuardValidator(RuleSet.AWS_PROTOTYPING)],});
// Use isto para implantar seu próprio ambiente sandbox (assume credenciais CLI)new ApplicationStack(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, crossRegionReferences: true,});
app.synth();
Este é o ponto de entrada para sua aplicação CDK.
Ele está configurado para usar cfn-guard
para executar validação de infraestrutura baseada no conjunto de regras configurado. Isso é instrumentado pós-synthesis.
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// O código que define sua stack vai aqui }}
Este é onde instanciaremos nossos constructs CDK para construir nosso jogo dungeon adventure.
Atualizar nossa infraestrutura
Vamos atualizar nosso packages/infra/src/stacks/application-stack.ts
para instanciar alguns de nossos constructs já gerados:
import { GameApi, GameUI, StoryApi, UserIdentity,} from ':dungeon-adventure/common-constructs';import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), }); const storyApi = new StoryApi(this, 'StoryApi', { integrations: StoryApi.defaultIntegrations(this).build(), });
// grant our authenticated role access to invoke our APIs [storyApi, gameApi].forEach((api) => api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole), );
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}
Note que aqui fornecemos integrações padrão para nossas duas APIs. Por padrão, cada operação em nossa API é mapeada para uma função lambda individual para lidar com essa operação.
Construindo nosso código
Comandos Nx
Targets únicos vs múltiplos
O comando run-many
executará um target em múltiplos subprojetos listados (--all
irá target todos). Ele garantirá que dependências sejam executadas na ordem correta.
Você também pode disparar um build (ou qualquer outra tarefa) para um target de projeto único executando o target diretamente no projeto. Por exemplo, se quisermos buildar o projeto @dungeon-adventure/infra
, você pode executar:
pnpm nx run @dungeon-adventure/infra:build
yarn nx run @dungeon-adventure/infra:build
npx nx run @dungeon-adventure/infra:build
bunx nx run @dungeon-adventure/infra:build
Visualizando dependências
Você também pode visualizar suas dependências via:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

Cache
O Nx utiliza cache para reutilizar artefatos de builds anteriores e acelerar o desenvolvimento. Alguma configuração é necessária para que isso funcione corretamente, e pode haver casos onde você quer executar um build sem usar o cache. Para isso, basta adicionar o argumento --skip-nx-cache
ao seu comando. Exemplo:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cache
yarn nx run @dungeon-adventure/infra:build --skip-nx-cache
npx nx run @dungeon-adventure/infra:build --skip-nx-cache
bunx nx run @dungeon-adventure/infra:build --skip-nx-cache
Se por qualquer motivo você quiser limpar seu cache (armazenado na pasta .nx
), execute:
pnpm nx reset
yarn nx reset
npx nx reset
bunx nx reset
pnpm nx run-many --target build --all
yarn nx run-many --target build --all
npx nx run-many --target build --all
bunx nx run-many --target build --all
Você deve ser questionado com o seguinte:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changes
Esta mensagem indica que o NX detectou alguns arquivos que podem ser atualizados automaticamente. Neste caso, refere-se aos arquivos tsconfig.json
que não têm referências TypeScript configuradas para projetos dependentes. Selecione a opção Yes, sync the changes and run the tasks para prosseguir. Você deve notar que todos os erros de importação relacionados ao IDE são resolvidos automaticamente, pois o gerador de sync adicionará as referências TypeScript faltantes!
Todos os artefatos construídos agora estão disponíveis na pasta dist/
na raiz do monorepositório. Esta é uma prática padrão ao usar projetos gerados pelo @aws/nx-plugin
, pois não polui sua árvore de arquivos com arquivos gerados. Caso queira limpar seus arquivos, basta deletar a pasta dist/
sem se preocupar com arquivos gerados espalhados.
Parabéns! Você criou todos os subprojetos necessários para começar a implementar o núcleo do nosso jogo Dunegeon Adventure. 🎉🎉🎉