Configurar un monorepo
Tarea 1: Crear un monorepo
Sección titulada «Tarea 1: Crear un monorepo»Para crear un nuevo monorepo, desde el directorio deseado, ejecuta el siguiente comando:
npx create-nx-workspace@21.6.8 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@21.6.8 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsEsto configurará un monorepo NX dentro del directorio dungeon-adventure. Cuando abras el directorio en VSCode, verás esta estructura de archivos:
Directorio.nx/
- …
Directorio.vscode/
- …
Directorionode_modules/
- …
Directoriopackages/ aquí residirán tus subproyectos
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configura el CLI de NX y los valores por defecto del monorepo
- package.json todas las dependencias de node se definen aquí
- pnpm-lock.yaml o bun.lock, yarn.lock, package-lock.json según el gestor de paquetes
- pnpm-workspace.yaml si usas pnpm
- README.md
- tsconfig.base.json todos los subproyectos basados en node extienden este
- tsconfig.json
- aws-nx-plugin.config.mts configuración para el Nx Plugin para AWS
Ahora podemos comenzar a crear nuestros diferentes subproyectos usando el @aws/nx-plugin.
Tarea 2: Crear una Game API
Sección titulada «Tarea 2: Crear una Game API»Primero, creemos nuestra Game API. Para esto, crea una API tRPC llamada GameApi usando estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#trpc-api - Complete los parámetros requeridos
- name: GameApi
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivenpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivebunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runVerás que aparecen algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#trpc-api
A continuación se muestra una lista de todos los archivos generados por el generador ts#trpc-api. Examinaremos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directorioapp/ constructs CDK específicos de la aplicación
Directorioapis/
- game-api.ts construct CDK para crear tu API tRPC
- index.ts
- …
- index.ts
Directoriocore/ constructs CDK genéricos
Directorioapi/
- rest-api.ts construct base CDK para API Gateway Rest API
- trpc-utils.ts utilidades para constructs CDK de API tRPC
- utils.ts utilidades para constructs de API
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Directoriotypes/ tipos compartidos
Directoriosrc/
- index.ts
- runtime-config.ts definición de interfaz usada tanto por CDK como por el sitio web
- project.json
- …
Directoriogame-api/ API tRPC
Directoriosrc/
Directorioclient/ cliente vanilla típicamente usado para llamadas máquina a máquina en TS
- index.ts
- sigv4.ts
Directoriomiddleware/ instrumentación con powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directorioschema/ definiciones de entradas y salidas para tu API
- echo.ts
Directorioprocedures/ implementaciones específicas de tus procedimientos/rutas de la API
- echo.ts
- index.ts
- init.ts configura el contexto y middleware
- local-server.ts usado al ejecutar el servidor tRPC localmente
- router.ts punto de entrada para tu lambda handler que define todos los procedimientos
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Veamos estos archivos clave:
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;El router define el punto de entrada para tu API tRPC y es donde declararás todos tus métodos de API. Como puedes ver arriba, tenemos un método llamado echo con su implementación en el archivo ./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 archivo es la implementación del método echo y como puedes ver está fuertemente tipado declarando sus estructuras de datos de entrada y salida.
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 las definiciones de esquema tRPC se definen usando Zod y se exportan como tipos TypeScript mediante la sintaxis 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ón de strings para todos los nombres de operaciones de la APItype Operations = Procedures<AppRouter>;
/** * Propiedades para crear un construct GameApi * * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nombres de operación a sus integraciones de API Gateway */ integrations: TIntegrations;}
/** * Un construct CDK que crea y configura una API REST de AWS API Gateway * específicamente para GameApi. * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Crea integraciones predeterminadas para todas las operaciones, que implementan cada operación como * su propia función lambda individual. * * @param scope - El alcance del construct CDK * @returns Un IntegrationBuilder con integraciones lambda predeterminadas */ 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: [ // Aquí permitimos que cualquier credencial AWS de la cuenta donde se despliega el proyecto pueda llamar a la API. // Se puede definir acceso fino máquina a máquina aquí usando principios más específicos (ej. roles o // usuarios) y recursos (ej. qué rutas de API pueden ser invocadas por qué principal) si es necesario. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Abrimos OPTIONS para permitir que los navegadores hagan solicitudes preflight sin autenticar new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}Este es el construct CDK que define nuestro GameApi. Proporciona un método defaultIntegrations que automáticamente crea una función Lambda para cada procedimiento en nuestra API tRPC, apuntando a la implementación de la API empaquetada. Esto significa que en el momento de cdk synth, no ocurre empaquetado (a diferencia de usar NodeJsFunction) ya que ya lo hemos empaquetado como parte del target de build del proyecto backend.
Tarea 3: Crear agentes de historia
Sección titulada «Tarea 3: Crear agentes de historia»Ahora creemos nuestros Story Agents.
Agente de historia: Proyecto Python
Sección titulada «Agente de historia: Proyecto Python»Para crear un proyecto Python:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#project - Complete los parámetros requeridos
- name: story
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactiveyarn nx g @aws/nx-plugin:py#project --name=story --no-interactivenpx nx g @aws/nx-plugin:py#project --name=story --no-interactivebunx nx g @aws/nx-plugin:py#project --name=story --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runVerás que aparecen algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por py#project
El generador py#project genera estos archivos:
Directorio.venv/ entorno virtual único para el monorepo
- …
Directoriopackages/
Directoriostory/
Directoriodungeon_adventure_story/ módulo Python
- hello.py archivo Python de ejemplo (lo ignoraremos)
Directoriotests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version versión de Python fijada para UV
- pyproject.toml
- uv.lock
Esto ha configurado un proyecto Python y un UV Workspace con entorno virtual compartido.
Agente de historia: Agente Strands
Sección titulada «Agente de historia: Agente Strands»Para agregar un agente Strands al proyecto con el generador py#strands-agent:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - py#strands-agent - Complete los parámetros requeridos
- project: story
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivenpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivebunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runVerás que aparecen algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por py#strands-agent
El generador py#strands-agent genera estos archivos:
Directoriopackages/
Directoriostory/
Directoriodungeon_adventure_story/ módulo Python
Directorioagent/
- main.py punto de entrada para tu agente en Bedrock AgentCore Runtime
- agent.py define un agente y herramientas de ejemplo
- agentcore_mcp_client.py utilidad para crear clientes que interactúen con servidores MCP
- Dockerfile define la imagen Docker para despliegue en AgentCore Runtime
Directoriocommon/constructs/
Directoriosrc
Directoriocore/agent-core/
- runtime.ts construct genérico para desplegar en AgentCore Runtime
Directorioapp/agents/story-agent/
- story-agent.ts construct para desplegar tu agente Story en AgentCore Runtime
Veamos algunos de los archivos en detalle:
from contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_time
# Define una herramienta personalizada@tooldef add(a: int, b: int) -> int: return a + b
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""Eres un mago de la suma.Usa la herramienta 'add' para tareas de suma.Refiérete a las herramientas como tu 'grimorio'.""", tools=[add, current_time], )Esto crea un agente Strands de ejemplo y define una herramienta de suma.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from .agent import get_agent
app = BedrockAgentCoreApp()
@app.entrypointasync def invoke(payload, context): """Manejador para invocación del agente""" prompt = payload.get( "prompt", "No se encontró prompt en la entrada, por favor guía al usuario " "a crear un payload JSON con la clave prompt" )
with get_agent(session_id=context.session_id) as agent: stream = agent.stream_async(prompt) async for event in stream: print(event) yield (event)
if __name__ == "__main__": app.run()Este es el punto de entrada para el agente, configurado usando el Amazon Bedrock AgentCore SDK. Utiliza soporte de Strands para streaming y transmite eventos de vuelta al cliente a medida que ocurren.
import { Lazy, Names } from 'aws-cdk-lib';import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';import { Construct } from 'constructs';import { execSync } from 'child_process';import * as path from 'path';import * as url from 'url';import { AgentCoreRuntime, AgentCoreRuntimeProps,} from '../../../core/agent-core/runtime.js';
export type StoryAgentProps = Omit< AgentCoreRuntimeProps, 'runtimeName' | 'serverProtocol' | 'containerUri'>;
export class StoryAgent extends Construct { public readonly dockerImage: DockerImageAsset; public readonly agentCoreRuntime: AgentCoreRuntime;
constructor(scope: Construct, id: string, props?: StoryAgentProps) { super(scope, id);
this.dockerImage = new DockerImageAsset(this, 'DockerImage', { platform: Platform.LINUX_ARM64, directory: path.dirname(url.fileURLToPath(new URL(import.meta.url))), extraHash: execSync( `docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`, { encoding: 'utf-8' }, ).trim(), });
this.agentCoreRuntime = new AgentCoreRuntime(this, 'StoryAgent', { runtimeName: Lazy.string({ produce: () => Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }), }), serverProtocol: 'HTTP', containerUri: this.dockerImage.imageUri, ...props, }); }}Esto configura un DockerImageAsset de CDK que sube tu imagen Docker del agente a ECR, y la aloja usando AgentCore Runtime.
Puedes notar un Dockerfile adicional, que referencia la imagen Docker del proyecto story, permitiéndonos ubicar conjuntamente el Dockerfile y el código fuente del agente.
Tarea 4: Configurar herramientas de inventario
Sección titulada «Tarea 4: Configurar herramientas de inventario»Inventario: Proyecto TypeScript
Sección titulada «Inventario: Proyecto TypeScript»Creemos un servidor MCP para proveer herramientas para que nuestro Story Agent gestione el inventario de un jugador.
Primero, creamos un proyecto TypeScript:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#project - Complete los parámetros requeridos
- name: inventory
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runEsto creará un proyecto TypeScript vacío.
Archivos actualizados por ts#project
El generador ts#project genera estos archivos.
Directoriopackages/
Directorioinventory/
Directoriosrc/
- index.ts punto de entrada con función de ejemplo
- project.json configuración del proyecto
- eslint.config.mjs configuración de lint
- vite.config.ts configuración de pruebas
- tsconfig.json configuración base de TypeScript para el proyecto
- tsconfig.lib.json configuración de TypeScript para compilación y empaquetado
- tsconfig.spec.json configuración de TypeScript para pruebas
- tsconfig.base.json actualizado para configurar un alias que otros proyectos usen para referenciar este
Inventario: Servidor MCP
Sección titulada «Inventario: Servidor MCP»Luego, agregaremos un servidor MCP a nuestro proyecto TypeScript:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#mcp-server - Complete los parámetros requeridos
- project: inventory
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runEsto agregará un servidor MCP.
Archivos actualizados por ts#mcp-server
El generador ts#mcp-server genera estos archivos.
Directoriopackages/
Directorioinventory/
Directoriosrc/mcp-server/
- server.ts crea el servidor MCP
Directoriotools/
- add.ts herramienta de ejemplo
Directorioresources/
- sample-guidance.ts recurso de ejemplo
- stdio.ts punto de entrada para MCP con transporte STDIO
- http.ts punto de entrada para MCP con transporte HTTP transmisible
- Dockerfile construye la imagen para AgentCore Runtime
- rolldown.config.ts configuración para empaquetar el servidor MCP para despliegue en AgentCore
Directoriocommon/constructs/
Directoriosrc
Directorioapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct para desplegar tu servidor MCP de inventario en AgentCore Runtime
Tarea 5: Crear la Interfaz de Usuario (UI)
Sección titulada «Tarea 5: Crear la Interfaz de Usuario (UI)»En esta tarea, crearemos la UI que te permitirá interactuar con el juego.
Interfaz del Juego: Sitio Web
Sección titulada «Interfaz del Juego: Sitio Web»Para crear la UI, crea un sitio web llamado GameUI usando estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#react-website - Complete los parámetros requeridos
- name: GameUI
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivenpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivebunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runVerás que aparecen algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#react-website
El generador ts#react-website genera estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directorioapp/ constructs CDK específicos de la aplicación
Directoriostatic-websites/
- game-ui.ts construct CDK para crear tu Game UI
Directoriocore/
- static-website.ts construct genérico para sitio web estático
Directoriogame-ui/
Directoriopublic/
- …
Directoriosrc/
Directoriocomponents/
DirectorioAppLayout/
- index.ts diseño general de página: encabezado, pie, barra lateral, etc
- navitems.ts elementos de navegación de la barra lateral
Directoriohooks/
- useAppLayout.tsx permite configurar dinámicamente notificaciones, estilo de página, etc
Directorioroutes/ rutas basadas en @tanstack/react-router
- index.tsx página raíz ’/’ redirige a ‘/welcome’
- __root.tsx todas las páginas usan este componente como base
Directoriowelcome/
- index.tsx
- config.ts
- main.tsx punto de entrada de React
- routeTree.gen.ts este archivo se actualiza automáticamente por @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, { websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}Este es el construct CDK que define nuestro GameUI. Ya ha configurado la ruta al bundle generado para nuestra UI basada en Vite. Esto significa que en el momento de build, el empaquetado ocurre dentro del target de build del proyecto game-ui y la salida se usa aquí.
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 la instancia del router para seguridad 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 es el punto de entrada donde se monta React. Como se muestra, inicialmente solo configura un @tanstack/react-router en una configuración de enrutamiento basado en archivos. Mientras tu servidor de desarrollo esté ejecutándose, puedes crear archivos dentro de la carpeta routes y @tanstack/react-router creará la configuración de archivos necesaria y actualizará el archivo routeTree.gen.ts. Este archivo mantiene todas las rutas de manera type-safe, lo que significa que cuando usas <Link>, la opción to solo mostrará rutas válidas.
Para más información, consulta la documentación de @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> );}Un componente se renderizará al navegar a la ruta /welcome. @tanstack/react-router gestionará la Route por ti cuando crees/muevas este archivo (siempre que el servidor de desarrollo esté ejecutándose).
Interfaz del Juego: Autenticación
Sección titulada «Interfaz del Juego: Autenticación»Configuremos nuestra Game UI para requerir acceso autenticado mediante Amazon Cognito usando estos pasos:
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#react-website#auth - Complete los parámetros requeridos
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivenpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivebunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runVerás que aparecen/cambian algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#react-website#auth
El generador ts#react-website#auth actualiza/genera estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directoriocore/
- user-identity.ts construct CDK para crear pools de usuarios/identidad
Directoriotypes/
Directoriosrc/
- runtime-config.ts actualizado para agregar cognitoProps
Directoriogame-ui/
Directoriosrc/
Directoriocomponents/
DirectorioAppLayout/
- index.tsx agrega el usuario conectado/cierre de sesión al encabezado
DirectorioCognitoAuth/
- index.ts gestiona el inicio de sesión en Cognito
DirectorioRuntimeConfig/
- index.tsx obtiene el
runtime-config.jsony lo provee a los hijos vía contexto
- index.tsx obtiene el
Directoriohooks/
- useRuntimeConfig.tsx
- main.tsx Actualizado para agregar 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 la instancia del router para seguridad 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>, );Los componentes RuntimeConfigProvider y CognitoAuth han sido agregados al archivo main.tsx mediante una transformación AST. Esto permite que el componente CognitoAuth se autentique con Amazon Cognito obteniendo el runtime-config.json que contiene la configuración de conexión a Cognito necesaria para realizar las llamadas al backend al destino correcto.
Interfaz del Juego: Conectar a Game API
Sección titulada «Interfaz del Juego: Conectar a Game API»Configuremos nuestra Game UI para conectarse a nuestra Game API creada previamente.
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - api-connection - Complete los parámetros requeridos
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivenpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivebunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runnpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runbunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runVerás que aparecen/cambian algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por la conexión UI -> tRPC
El generador api-connection genera/actualiza estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriogame-ui/
Directoriosrc/
Directoriocomponents/
- GameApiClientProvider.tsx configura el cliente de GameAPI
Directoriohooks/
- useGameApi.tsx hooks para llamar a la GameApi
- main.tsx inyecta los proveedores del cliente trpc
- package.json
import { GameApiTRCPContext } from '../components/GameApiClientProvider';
export const useGameApi = GameApiTRCPContext.useTRPC;Este hook usa la última integración de React Query de tRPC permitiendo a los usuarios interactuar con @tanstack/react-query directamente sin capas adicionales de abstracción. Para ejemplos de cómo llamar a APIs tRPC, consulta la guía de uso del 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 la instancia del router para seguridad 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> <GameApiClientProvider> <RouterProvider router={router} /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );El archivo main.tsx ha sido actualizado mediante una transformación AST para inyectar los proveedores de tRPC.
Interfaz del Juego: Infraestructura
Sección titulada «Interfaz del Juego: Infraestructura»Creemos el último subproyecto para la infraestructura CDK.
- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#infra - Complete los parámetros requeridos
- name: infra
- Haga clic en
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runVerás que aparecen/cambian algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#infra
El generador ts#infra genera/actualiza estos archivos. Examinemos algunos de los archivos clave resaltados en el árbol de archivos:
Directoriopackages/
Directoriocommon/
Directorioconstructs/
Directoriosrc/
Directoriocore/
- checkov.ts
- index.ts
Directorioinfra
Directoriosrc/
Directoriostages/
- application-stage.ts stacks CDK definidos aquí
Directoriostacks/
- application-stack.ts recursos CDK definidos aquí
- index.ts
- main.ts punto de entrada que define todas las etapas
- cdk.json
- project.json
- …
- package.json
- tsconfig.json agregadas referencias
- tsconfig.base.json agregado alias
import { ApplicationStage } from './stacks/application-stage.js';import { App } from ':dungeon-adventure/common-constructs';
const app = new App();
// Usa esto para desplegar tu propio entorno sandbox (asume tus credenciales CLI)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();Este es el punto de entrada para tu aplicación CDK.
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);
// El código que define tu stack va aquí }}Instanciaremos nuestros constructs CDK para construir nuestro juego de aventuras.
Tarea 6: Actualizar nuestra infraestructura
Sección titulada «Tarea 6: Actualizar nuestra infraestructura»Actualicemos packages/infra/src/stacks/application-stack.ts para instanciar algunos de nuestros constructs generados:
import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}import { Stack, StackProps } from 'aws-cdk-lib';import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: 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 { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}Tarea 7: Construir el código
Sección titulada «Tarea 7: Construir el código»Comandos de Nx
Targets únicos vs múltiples
Sección titulada «Targets únicos vs múltiples»El comando run-many ejecutará un target en múltiples subproyectos listados (--all los seleccionará todos). Esto asegura que las dependencias se ejecuten en el orden correcto.
También puedes disparar un build (o cualquier otra tarea) para un target de proyecto único ejecutando el target directamente en el proyecto. Por ejemplo, para construir el proyecto @dungeon-adventure/infra, ejecuta el siguiente comando:
pnpm nx run @dungeon-adventure/infra:buildyarn nx run @dungeon-adventure/infra:buildnpx nx run @dungeon-adventure/infra:buildbunx nx run @dungeon-adventure/infra:buildTambién puedes omitir el scope y usar la sintaxis abreviada de Nx si prefieres:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraVisualizando tus dependencias
Sección titulada «Visualizando tus dependencias»Para visualizar tus dependencias, ejecuta:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Nx depende del caching para reutilizar artefactos de builds previos y acelerar el desarrollo. Se requiere cierta configuración para que esto funcione correctamente y puede haber casos donde quieras realizar un build sin usar la caché. Para eso, simplemente agrega el argumento --skip-nx-cache a tu comando. Por ejemplo:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cacheyarn nx run @dungeon-adventure/infra:build --skip-nx-cachenpx nx run @dungeon-adventure/infra:build --skip-nx-cachebunx nx run @dungeon-adventure/infra:build --skip-nx-cacheSi por alguna razón quisieras limpiar tu caché (almacenada en la carpeta .nx), puedes ejecutar el siguiente comando:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetUsando la línea de comandos, ejecuta:
pnpm nx run-many --target build --allyarn nx run-many --target build --allnpx nx run-many --target build --allbunx nx run-many --target build --allSe te solicitará lo siguiente:
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 changesEste mensaje indica que NX ha detectado algunos archivos que pueden actualizarse automáticamente. En este caso, se refiere a los archivos tsconfig.json que no tienen referencias de proyecto configuradas en proyectos dependientes.
Selecciona la opción Yes, sync the changes and run the tasks para continuar. Deberías notar que todos los errores de importación en tu IDE se resuelven automáticamente, ya que el generador de sync agregará las referencias TypeScript faltantes.
Todos los artefactos construidos están ahora disponibles dentro de la carpeta dist/ ubicada en la raíz del monorepo. Esta es una práctica estándar cuando se usan proyectos generados por el @aws/nx-plugin ya que no contamina tu árbol de archivos con archivos generados. En caso de que quieras limpiar tus archivos, elimina la carpeta dist/ sin preocuparte por archivos generados esparcidos por el árbol de directorios.
¡Felicidades! Has creado todos los subproyectos requeridos para comenzar a implementar el núcleo de nuestro juego AI Dungeon Adventure. 🎉🎉🎉