Jogo de Dungeons com IA
Módulo 1: Configuração do monorepo
Seção intitulada “Módulo 1: Configuração do monorepo”Vamos começar criando um novo monorepo. 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 monorepo 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 monorepo
- package.json todas as dependências do 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 baseados em Node estendem este
- tsconfig.json
Agora estamos prontos para começar a criar nossos diferentes subprojetos usando o @aws/nx-plugin
.
Game API
Seção intitulada “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 do 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 CDK 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/ API tRPC
Directorysrc/
Directoryclient/ cliente vanilla tipicamente usado para chamadas máquina a máquina em TS
- index.ts
- sigv4.ts
Directorymiddleware/ instrumentação powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definições de entradas e saídas da API
- echo.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
- …
- 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 '../schema/echo.js';
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 ao declarar suas estruturas de dados de entrada e saída.
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 de união string 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 integrações do 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/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 de implantação 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 requisições preflight não autenticadas de navegadores 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
Seção intitulada “Story API”Agora vamos criar nossa Story API. Para isso, vamos criar uma API Fast 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
- moduleName: story_api
- Clique em
Generate
pnpm nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=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:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive --dry-run
yarn nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive --dry-run
npx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive --dry-run
bunx nx g @aws/nx-plugin:py#fast-api --name=StoryApi --moduleName=story_api --no-interactive --dry-run
Você deve ver alguns novos arquivos aparecerem em sua árvore de arquivos.
Arquivos atualizados do 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 monorepo
- …
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos do aplicativo
Directoryapis/
- story-api.ts construct CDK para criar sua API Fast
- 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 o 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 fixada do 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 integrações do 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 de implantação 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 requisições preflight não autenticadas de navegadores 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 do 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. Veja que o StoryApi
foi adicionado à definição IRuntimeConfig
, o que significa que quando consumido pelo frontend, imporá type safety!
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 type safety.
Game UI: Website
Seção intitulada “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 do 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 seu 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 do @tanstack/react-router
- index.tsx página raiz ’/’ redireciona para ‘/welcome’
- __root.tsx todas as 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, já está configurado com 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 type safetydeclare 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 file-based-routing
. 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
Seção intitulada “Game UI: Auth”Agora vamos configurar nosso 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/alterarem em sua árvore de arquivos.
Arquivos atualizados do 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 via context
- 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 type safetydeclare 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 backend ao destino correto.
Game UI: Conectar à Story API
Seção intitulada “Game UI: Conectar à Story API”Agora vamos configurar nosso 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/alterarem em sua árvore de arquivos.
Arquivos atualizados da 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 pelo StoryApi para assinar requisições
- useStoryApiClient.tsx hook para construir um cliente StoryApi
- useStoryApi.tsx hook para interagir com StoryApi usando TanStack Query
Directorycomponents/
- QueryClientProvider.tsx provedor do cliente TanStack Query
- StoryApiProvider.tsx Provedor para o hook TanStack Query do StoryApi
- main.tsx Instrumenta o QueryClientProvider e StoryApiProvider
- .gitignore ignora arquivos client 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
gerado no build, então você verá um erro no IDE até construirmos nosso código. Para mais detalhes sobre geração do client ou consumo da 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;
Este componente provedor usa o hook useStoryApiClient
e instancia o StoryApiOptionsProxy
, usado para construir opções para hooks TanStack Query. Você pode usar o hook correspondente useStoryApi
para acessar este proxy de opções, que fornece uma maneira consistente de interagir com sua FastAPI e API tRPC.
Como useStoryApiClient
nos fornece um async iterator para nossa API de streaming, usaremos o client vanilla diretamente neste tutorial.
Game UI: Conectar à Game API
Seção intitulada “Game UI: Conectar à Game API”Agora vamos configurar nosso 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/alterarem em sua árvore de arquivos.
Arquivos atualizados da 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/
- GameApiClientProvider.tsx configura o client GameAPI
Directoryhooks/
- useGameApi.tsx hooks para chamar a GameApi
- main.tsx injeta os provedores de client trpc
- package.json
import { useGameApi as useClient } from '../components/GameApiClientProvider';
export const useGameApi = useClient;
Este hook usa a integração React Query do tRPC mais recente, permitindo que usuários interajam diretamente com @tanstack/react-query
sem camadas adicionais de abstração. Para exemplos de uso, consulte o guia de uso do hook tRPC.
import GameApiClientProvider from './components/GameApiClientProvider';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 type safetydeclare 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> <GameApiClientProvider> <RouterProvider router={router} /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );
O arquivo main.tsx
foi atualizado via transformação AST para injetar os provedores tRPC.
Game UI: Infraestrutura
Seção intitulada “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/alterarem em sua árvore de arquivos.
Arquivos atualizados do 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
Seção intitulada “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 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
Seção intitulada “Construindo nosso código”Comandos Nx
Targets únicos vs múltiplos
Seção intitulada “Targets únicos vs múltiplos”O comando run-many
executará um target em múltiplos subprojetos listados (--all
os incluirá todos). Ele garantirá que dependências sejam executadas na ordem correta.
Você também pode disparar um build (ou qualquer outra task) 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
Seção intitulada “Visualizando dependências”Você também pode visualizar suas dependências via:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

O Nx usa cache para reutilizar artefatos de builds anteriores e acelerar o desenvolvimento. Há configurações necessárias para isso funcionar corretamente e casos onde você pode querer executar um build sem usar o cache. Para isso, adicione o argumento --skip-nx-cache
ao 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 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 Yes, sync the changes and run the tasks para prosseguir. Você deve notar que todos os erros de importação no IDE são resolvidos automaticamente, pois o gerador de sync adicionará as referências TypeScript faltantes!
Todos os artefatos construídos estão agora disponíveis na pasta dist/
na raiz do monorepo. 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. 🎉🎉🎉