Juego de Mazmorra con IA
Módulo 1: Configuración del monorepositorio
Sección titulada «Módulo 1: Configuración del monorepositorio»Comenzaremos creando un nuevo monorepositorio. Desde el directorio deseado, ejecuta el siguiente 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
Esto configurará un monorepositorio NX dentro del directorio dungeon-adventure
que podrás abrir en VSCode. Debería verse así:
Directory.nx/
- …
Directory.vscode/
- …
Directorynode_modules/
- …
Directorypackages/ aquí residirán tus subproyectos
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json configura los valores predeterminados de la CLI de Nx y el monorepositorio
- package.json aquí se definen todas las dependencias de Node
- 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 archivo
- tsconfig.json
Ahora estamos listos para comenzar a crear nuestros diferentes subproyectos usando el @aws/nx-plugin
.
API del Juego
Sección titulada «API del Juego»Primero creemos nuestra API del Juego. Para esto, crearemos una API tRPC llamada GameApi
siguiendo 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-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
Tambié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-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
Deberías ver que han aparecido 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
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directoryapis/
- game-api.ts constructo CDK para crear tu API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs CDK genéricos
Directoryapi/
- rest-api.ts constructo base CDK para una API Rest de API Gateway
- 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
- …
Directorytypes/ tipos compartidos
Directorysrc/
- index.ts
- runtime-config.ts definición de interfaz usada tanto por CDK como por el sitio web
- project.json
- …
Directorygame-api/ API tRPC
Directorysrc/
Directoryclient/ cliente vanilla típicamente usado para llamadas máquina a máquina en TS
- index.ts
- sigv4.ts
Directorymiddleware/ instrumentación con Powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Directoryschema/ definiciones de entradas y salidas para tu API
- echo.ts
Directoryprocedures/ implementaciones específicas de los procedimientos/rutas de tu 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
Echemos un vistazo a algunos de los 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 los métodos de tu 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 al declarar 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 constructo 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 constructo 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 constructo 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 detallado 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 no autenticadas new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}
Este es el constructo CDK que define nuestro GameApi. Como puedes ver, proporciona un método defaultIntegrations
que crea automáticamente 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 objetivo de compilación del proyecto backend.
API de la Historia
Sección titulada «API de la Historia»Ahora creemos nuestra API de la Historia. Para esto, crearemos una API Fast llamada StoryApi
siguiendo 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 - py#fast-api
- Complete los parámetros requeridos
- name: StoryApi
- moduleName: story_api
- Haga clic en
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
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
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
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por py#fast-api
A continuación se muestra una lista de todos los archivos generados por el generador py#fast-api
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directory.venv/ entorno virtual único para el monorepositorio
- …
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directoryapis/
- story-api.ts constructo CDK para crear tu API Fast
- index.ts actualizado para exportar el nuevo story-api
- project.json actualizado para agregar una dependencia de compilación en story_api
Directorytypes/ tipos compartidos
Directorysrc/
- runtime-config.ts actualizado para agregar el StoryApi
Directorystory_api/
Directorystory_api/ módulo Python
- init.py configura Powertools, FastAPI y middleware
- main.py punto de entrada para la lambda que contiene todas las rutas
Directorytests/
- …
- .python-version
- project.json
- pyproject.toml
- project.json
- .python-version versión de Python fijada 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';
/** * Propiedades para crear un constructo StoryApi * * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export interface StoryApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Mapa de nombres de operación a sus integraciones de API Gateway */ integrations: TIntegrations;}
/** * Un constructo CDK que crea y configura una API REST de AWS API Gateway * específicamente para StoryApi. * @template TIntegrations - Mapa de nombres de operación a sus integraciones */export class StoryApi< 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 constructo CDK * @returns Un IntegrationBuilder con integraciones lambda predeterminadas */ 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: [ // Aquí permitimos que cualquier credencial AWS de la cuenta donde se despliega el proyecto pueda llamar a la API. // Se puede definir acceso detallado 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 no autenticadas new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: OPERATION_DETAILS, ...props, }); }}
Este es el constructo CDK que define nuestro StoryApi. Como puedes ver, proporciona un método defaultIntegrations
que crea automáticamente una función lambda para cada operación definida en nuestra FastAPI, apuntando a la implementación de la API empaquetada. Esto significa que en el momento de cdk synth
, no ocurre empaquetado (a diferencia de PythonFunction) ya que ya lo hemos empaquetado como parte del objetivo de compilación del proyecto 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; };}
Aquí hay un ejemplo del generador realizando una transformación AST que preserva todo el código existente y realiza una actualización. Puedes ver que se agregó StoryApi
a la definición de IRuntimeConfig
, lo que significa que cuando esto sea consumido por nuestro frontend, ¡se aplicará la seguridad de tipos!
from .init import app, lambda_handler, tracer
handler = lambda_handler
@app.get("/")@tracer.capture_methoddef read_root(): return {"Hello": "World"}
Aquí es donde se definirán todos tus métodos de API. Como puedes ver aquí, tenemos un método read_root
mapeado a la ruta GET /
. Puedes usar Pydantic para declarar las entradas y salidas de tus métodos y garantizar la seguridad de tipos.
Interfaz de Usuario del Juego: Sitio Web
Sección titulada «Interfaz de Usuario del Juego: Sitio Web»Ahora creemos la interfaz de usuario que te permitirá interactuar con el juego. Para esto, creemos un sitio web llamado GameUI
siguiendo 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#cloudscape-website
- Complete los parámetros requeridos
- name: GameUI
- Haga clic en
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
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
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
Deberías ver que han aparecido algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#cloudscape-website
A continuación se muestra una lista de todos los archivos generados por el generador ts#cloudscape-website
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directorystatic-websites/
- game-ui.ts constructo CDK para crear tu Game UI
Directorycore/
- static-website.ts constructo genérico para sitio web estático
Directorygame-ui/
Directorypublic/
- …
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.ts diseño general de la página: encabezado, pie, barra lateral, etc.
- navitems.ts elementos de navegación de la barra lateral
Directoryhooks/
- useAppLayout.tsx permite configurar dinámicamente notificaciones, estilo de página, etc.
Directoryroutes/ rutas basadas en archivos de @tanstack/react-router
- index.tsx página raíz ’/’ redirige a ‘/welcome’
- __root.tsx todas las páginas usan este componente como base
Directorywelcome/
- 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, { websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}
Este es el constructo CDK que define nuestro GameUI. Como puedes ver, ya está configurada la ruta al paquete generado para nuestra UI basada en Vite. Esto significa que en el momento de build
, el empaquetado ocurre dentro del objetivo de compilación del proyecto game-ui y su 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
. Esto significa que, mientras tu servidor de desarrollo esté en ejecución, simplemente 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 segura en tipos, lo que significa que cuando uses <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>Bienvenido</Header>}> <SpaceBetween size="l"> <Container>¡Bienvenido a tu nuevo sitio web Cloudscape!</Container> </SpaceBetween> </ContentLayout> );}
Un componente que se renderizará al navegar a la ruta /welcome
. @tanstack/react-router
gestionará la Route
por ti cada vez que crees/muevas este archivo (siempre que el servidor de desarrollo esté en ejecución). Esto se mostrará en una sección posterior de este tutorial.
Interfaz de Usuario del Juego: Autenticación
Sección titulada «Interfaz de Usuario del Juego: Autenticación»Ahora configuremos nuestra Game UI para requerir acceso autenticado mediante Amazon Cognito siguiendo 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#cloudscape-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#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
También puede realizar una ejecución en seco para ver qué archivos se cambiarían
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
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#cloudscape-website#auth
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador ts#cloudscape-website#auth
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts constructo CDK para crear pools de usuarios/identidad
Directorytypes/
Directorysrc/
- runtime-config.ts actualizado para agregar cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx agrega el usuario conectado/cierre de sesión al encabezado
DirectoryCognitoAuth/
- index.ts gestiona el inicio de sesión en Cognito
DirectoryRuntimeConfig/
- index.tsx obtiene el
runtime-config.json
y lo provee a los hijos mediante contexto
- index.tsx obtiene el
Directoryhooks/
- 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
se han agregado 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 de Cognito necesaria para realizar las llamadas al backend al destino correcto.
Interfaz de Usuario del Juego: Conectar a Story API
Sección titulada «Interfaz de Usuario del Juego: Conectar a Story API»Ahora configuremos nuestra Game UI para conectarse a nuestra Story 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.story_api
- Haga clic en
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
Tambié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.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
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por la conexión UI -> FastAPI
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador api-connection
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directoryhooks/
- useSigV4.tsx usado por StoryApi para firmar solicitudes
- useStoryApiClient.tsx hook para construir un cliente StoryApi
- useStoryApi.tsx hook para interactuar con StoryApi usando TanStack Query
Directorycomponents/
- QueryClientProvider.tsx proveedor del cliente TanStack Query
- StoryApiProvider.tsx Proveedor para el hook TanStack Query de StoryApi
- main.tsx Instrumenta QueryClientProvider y StoryApiProvider
- .gitignore ignora archivos de cliente generados
- project.json actualizado para agregar objetivos de generación de hooks openapi
- …
Directorystory_api/
Directoryscripts/
- generate_open_api.py
- project.json actualizado para emitir un archivo 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 se puede usar para hacer solicitudes de API autenticadas a StoryApi
. Como puedes ver en la implementación, usa el StoryApi
que se genera en tiempo de compilación, por lo que verás un error en tu IDE hasta que compilemos nuestro código. Para más detalles sobre cómo se genera el cliente o cómo consumir la API, consulta la guía de React a 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;
El componente proveedor anterior usa el hook useStoryApiClient
e instancia el StoryApiOptionsProxy
, que se usa para construir opciones para los hooks de TanStack Query. Puedes usar el hook correspondiente useStoryApi
para acceder a este proxy de opciones, que proporciona una manera de interactuar con tu FastAPI de manera consistente con tu API tRPC.
Dado que useStoryApiClient
nos proporciona un iterador asíncrono para nuestra API de streaming, en este tutorial usaremos directamente el cliente vanilla.
Interfaz de Usuario del Juego: Conectar a Game API
Sección titulada «Interfaz de Usuario del Juego: Conectar a Game API»Ahora 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-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
Tambié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-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
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por la conexión UI -> tRPC
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador api-connection
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorygame-ui/
Directorysrc/
Directorycomponents/
- GameApiClientProvider.tsx configura el cliente GameAPI
Directoryhooks/
- useGameApi.tsx hooks para llamar a GameApi
- main.tsx inyecta los proveedores del cliente trpc
- package.json
import { useGameApi as useClient } from '../components/GameApiClientProvider';
export const useGameApi = useClient;
Este hook usa la última integración de React Query de tRPC, permitiendo a los usuarios interactuar directamente con @tanstack/react-query
sin capas adicionales de abstracción. Para ejemplos de cómo llamar a las 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
se ha actualizado mediante una transformación AST para inyectar los proveedores de tRPC.
Infraestructura de la Interfaz de Usuario del Juego
Sección titulada «Infraestructura de la Interfaz de Usuario del Juego»Ahora el último subproyecto que necesitamos crear es para la infraestructura CDK. Para crearlo, sigue 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#infra
- Complete los parámetros requeridos
- name: infra
- Haga clic en
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
Tambié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-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
Deberías ver que han aparecido/cambiado algunos archivos nuevos en tu árbol de directorios.
Archivos actualizados por ts#infra
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador ts#infra
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
Directorycfn-guard-rules/
- *.guard
- cfn-guard.ts
- index.ts
Directoryinfra
Directorysrc/
Directorystacks/
- application-stack.ts recursos CDK definidos aquí
- index.ts
- main.ts punto de entrada que define todos los stacks
- cdk.json
- project.json
- …
- package.json
- tsconfig.json agregar referencias
- tsconfig.base.json agregar 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)],});
// Usa esto para desplegar tu propio entorno sandbox (asume las credenciales de tu 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 es el punto de entrada para tu aplicación CDK.
Está configurado para usar cfn-guard
y ejecutar validación de infraestructura basada en el conjunto de reglas configurado. Esto se instrumenta post síntesis.
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í }}
Aquí es donde instanciaremos nuestros constructos CDK para construir nuestro juego de aventuras.
Actualizar nuestra infraestructura
Sección titulada «Actualizar nuestra infraestructura»Actualicemos nuestro packages/infra/src/stacks/application-stack.ts
para instanciar algunos de nuestros constructos ya generados:
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'); }}
Observa que aquí proporcionamos integraciones predeterminadas para nuestras dos APIs. Por defecto, cada operación en nuestra API se mapea a una función lambda individual para manejar esa operación.
Construyendo nuestro código
Sección titulada «Construyendo nuestro código»Comandos de Nx
Objetivos únicos vs múltiples
Sección titulada «Objetivos únicos vs múltiples»El comando run-many
ejecutará un objetivo en múltiples subproyectos listados (--all
los seleccionará todos). Se asegurará de que las dependencias se ejecuten en el orden correcto.
También puedes activar un build (o cualquier otra tarea) para un objetivo de proyecto único ejecutando el objetivo directamente en el proyecto. Por ejemplo, si queremos construir el proyecto @dungeon-adventure/infra
, puedes ejecutar el siguiente comando:
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 tus dependencias
Sección titulada «Visualizando tus dependencias»También puedes visualizar tus dependencias mediante:
pnpm nx graph
yarn nx graph
npx nx graph
bunx nx graph

Nx depende del caching para que puedas reutilizar artefactos de builds anteriores 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-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
Si por alguna razón quisieras limpiar tu caché (almacenada en la carpeta .nx
), puedes ejecutar el siguiente comando:
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
Deberías recibir el siguiente mensaje:
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
Este 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 TypeScript configuradas en proyectos referenciados. 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 sincronización agregará las referencias de TypeScript faltantes.
Todos los artefactos construidos están ahora disponibles dentro de la carpeta dist/
ubicada en la raíz del monorepositorio. 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, simplemente puedes eliminar la carpeta dist/
sin preocuparte por archivos generados esparcidos por el árbol de directorios.
¡Felicidades! Has creado todos los subproyectos necesarios para comenzar a implementar el núcleo de nuestro juego Dunegeon Adventure. 🎉🎉🎉