Juego de Mazmorra con IA
Módulo 1: Configuración del monorepo
Sección titulada «Módulo 1: Configuración del monorepo»Comenzaremos creando un nuevo monorepo. 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 monorepo 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 el CLI de Nx y los valores predeterminados 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
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, creemos 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 nuevos archivos 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 construct CDK para crear tu API tRPC
- index.ts
- …
- index.ts
Directorycore/ constructs CDK genéricos
Directoryapi/
- rest-api.ts construct 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 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 al declarar sus estructuras de datos de entrada y salida.
import { z } from 'zod/v4';
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 aquí acceso granular máquina a máquina 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. Como puedes ver, 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.
API de Historia
Sección titulada «API de Historia»Ahora creemos nuestra API de Historia. Para esto, creemos 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 nuevos archivos 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 monorepo
- …
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directoryapp/ constructs CDK específicos de la aplicación
Directoryapis/
- story-api.ts construct CDK para crear tu API Fast
- index.ts actualizado para exportar el nuevo story-api
- project.json actualizado para añadir una dependencia de build en story_api
Directorytypes/ tipos compartidos
Directorysrc/
- runtime-config.ts actualizado para añadir 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 construct 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 construct 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 construct 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 aquí acceso granular máquina a máquina 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: OPERATION_DETAILS, ...props, }); }}
Este es el construct CDK que define nuestro StoryApi. Como puedes ver, proporciona un método defaultIntegrations
que automáticamente crea 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 target de build 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 añadió StoryApi
a la definición de IRuntimeConfig
, lo que significa que cuando esto sea consumido por nuestro frontend, ¡impondrá 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 asegurar 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 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#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-interactive
yarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
npx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive
bunx nx g @aws/nx-plugin:ts#react-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#react-website --name=GameUI --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-run
Deberías ver que han aparecido nuevos archivos en tu árbol de directorios.
Archivos actualizados por ts#react-website
A continuación se muestra una lista de todos los archivos generados por el generador ts#react-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 construct CDK para crear tu Game UI
Directorycore/
- static-website.ts construct genérico para sitios web estáticos
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 todos 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 construct CDK que define nuestro GameUI. Como puedes ver, ya está configurada 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 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 enrutado 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>Welcome</Header>}> <SpaceBetween size="l"> <Container>Welcome to your new Cloudscape website!</Container> </SpaceBetween> </ContentLayout> );}
Un componente que 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é 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#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-interactive
yarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
npx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive
bunx nx g @aws/nx-plugin:ts#react-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#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
yarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
npx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
bunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-run
Deberías ver que han aparecido/cambiado nuevos archivos en tu árbol de directorios.
Archivos actualizados por ts#react-website#auth
A continuación se muestra una lista de todos los archivos generados/actualizados por el generador ts#react-website#auth
. Vamos a examinar algunos de los archivos clave resaltados en el árbol de archivos:
Directorypackages/
Directorycommon/
Directoryconstructs/
Directorysrc/
Directorycore/
- user-identity.ts construct CDK para crear pools de usuarios/identidad
Directorytypes/
Directorysrc/
- runtime-config.ts actualizado para añadir cognitoProps
Directorygame-ui/
Directorysrc/
Directorycomponents/
DirectoryAppLayout/
- index.tsx añade 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 añadir 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 añadido 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 conectarnos 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 nuevos archivos 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 añadir targets para generar 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 puede usarse para hacer solicitudes API autenticadas a StoryApi
. Como puedes ver en la implementación, usa el StoryApi
que se genera en tiempo de build, 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 provee un iterador asíncrono para nuestra API de streaming, en este tutorial simplemente usaremos el cliente vanilla directamente.
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 conectarnos 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 nuevos archivos 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 de 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 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 de Usuario del Juego: Infraestructura
Sección titulada «Interfaz de Usuario del Juego: Infraestructura»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 nuevos archivos 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 añade referencias
- tsconfig.base.json añade 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 credenciales 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
para 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 constructs CDK para construir nuestro juego de aventuras.
Actualizar nuestra infraestructura
Sección titulada «Actualizar nuestra infraestructura»Hagamos una actualización en nuestro packages/infra/src/stacks/application-stack.ts
para instanciar algunos de nuestros constructs 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'); }}
Nota que aquí proveemos integraciones predeterminadas para nuestras dos APIs. Por defecto, cada operación en nuestra API está mapeada 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
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). Se asegurará de 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, 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 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 añade 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 prompt:
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 proceder. ¡Deberías notar que todos los errores de importación relacionados con tu IDE se resuelven automáticamente ya que el generador de sync añadirá las referencias de typescript faltantes automáticamente!
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, simplemente puedes borrar la carpeta dist/
sin preocuparte de 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 Dunegeon Adventure. 🎉🎉🎉