@aws/nx-plugin-mcp 1.0.0-rc.37 → 1.0.0-rc.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,405 @@
1
+ ---
2
+ title: Contribute a Generator
3
+ description: A walkthrough of how to build a generator using the @aws/nx-plugin.
4
+ ---
5
+
6
+ import {
7
+ Aside,
8
+ Code,
9
+ FileTree,
10
+ Steps,
11
+ Tabs,
12
+ TabItem,
13
+ } from '@astrojs/starlight/components';
14
+ import { Image } from 'astro:assets';
15
+ import Drawer from '@components/drawer.astro';
16
+ import Link from '@components/link.astro';
17
+ import RunGenerator from '@components/run-generator.astro';
18
+ import NxCommands from '@components/nx-commands.astro';
19
+ import LinkCommand from '@components/link-command.astro';
20
+ import CreateNxWorkspaceCommand from '@components/create-nx-workspace-command.astro';
21
+ import InstallCommand from '@components/install-command.astro';
22
+ import dungeonAdventureArchitecturePng from '@assets/dungeon-game-architecture.png';
23
+ import baselineWebsitePng from '@assets/baseline-website.png';
24
+ import baselineGamePng from '@assets/baseline-game.png';
25
+ import nxGraphPng from '@assets/nx-graph.png';
26
+ import gameSelectPng from '@assets/game-select.png';
27
+ import gameConversationPng from '@assets/game-conversation.png';
28
+
29
+ :::tip[Learning By Example]
30
+ This tutorial guides you through contributing a generator to the Nx Plugin for AWS with a practical example. It can also serve as useful reference for ways to think about and test generators. You can skip to the general guidance section for some top tips for working on generators.
31
+ :::
32
+
33
+ Let's create a new generator to contribute to `@aws/nx-plugin`. Our objective will be to generate a new procedure for a tRPC API.
34
+
35
+ ### Check Out the Plugin
36
+
37
+ First, let's clone the plugin:
38
+
39
+ ```bash
40
+ git clone git@github.com:awslabs/nx-plugin-for-aws.git
41
+ ```
42
+
43
+ Next, install and build:
44
+
45
+ ```bash
46
+ cd nx-plugin-for-aws
47
+ pnpm i
48
+ pnpm nx run-many --target build --all
49
+ ```
50
+
51
+ ### Create an Empty Generator
52
+
53
+ Let's create the new generator in `packages/nx-plugin/src/trpc/procedure`.
54
+
55
+ We provide a generator for creating new generators so you can quickly scaffold your new generator! You can run this generator as follows:
56
+
57
+ <RunGenerator generator="ts#nx-generator" requiredParameters={{ pluginProject: '@aws/nx-plugin', name: 'ts#trpc-api#procedure', directory: 'trpc/procedure', description: 'Adds a procedure to a tRPC API' }} />
58
+
59
+ You will notice the following files have been generated for you:
60
+
61
+ <FileTree>
62
+ - packages/nx-plugin/src/trpc/procedure
63
+ - schema.json Defines the input for the generator
64
+ - schema.d.ts A typescript interface which matches the schema
65
+ - generator.ts Function which Nx runs as the generator
66
+ - generator.spec.ts Tests for the generator
67
+ - docs/src/content/docs/guides/
68
+ - trpc-procedure.mdx Documentation for the generator
69
+ - packages/nx-plugin/generators.json Updated to include the generator
70
+ </FileTree>
71
+
72
+ Let's update the schema to add the properties we'll need for the generator:
73
+
74
+ <Tabs>
75
+ <TabItem label="schema.json">
76
+ ```json
77
+ {
78
+ "$schema": "https://json-schema.org/schema",
79
+ "$id": "tRPCProcedure",
80
+ "title": "Adds a procedure to a tRPC API",
81
+ "type": "object",
82
+ "properties": {
83
+ "project": {
84
+ "type": "string",
85
+ "description": "tRPC API project",
86
+ "x-prompt": "Select the tRPC API project to add the procedure to",
87
+ "x-dropdown": "projects",
88
+ "x-priority": "important"
89
+ },
90
+ "procedure": {
91
+ "description": "The name of the new procedure",
92
+ "type": "string",
93
+ "x-prompt": "What would you like to call your new procedure?",
94
+ "x-priority": "important",
95
+ },
96
+ "type": {
97
+ "description": "The type of procedure to generate",
98
+ "type": "string",
99
+ "x-prompt": "What type of procedure would you like to generate?",
100
+ "x-priority": "important",
101
+ "default": "query",
102
+ "enum": ["query", "mutation"]
103
+ }
104
+ },
105
+ "required": ["project", "procedure"]
106
+ }
107
+ ```
108
+ </TabItem>
109
+ <TabItem label="schema.d.ts">
110
+ ```ts
111
+ export interface TrpcProcedureSchema {
112
+ project: string;
113
+ procedure: string;
114
+ type: 'query' | 'mutation';
115
+ }
116
+ ```
117
+ </TabItem>
118
+ </Tabs>
119
+
120
+ :::note[Virtual File System]
121
+ Notice the generator is given a `Tree` as input, as well as the options we defined in our schema. The `Tree` is essentially a virtual file system which we can read from and write to in order to create or update project files. We don't want to touch the filesystem directly, as we don't want to make any changes if users run the generator in "dry-run" mode.
122
+ :::
123
+
124
+ You will notice the generator has already been hooked up in `packages/nx-plugin/generators.json`:
125
+
126
+ ```json
127
+ ...
128
+ "generators": {
129
+ ...
130
+ "ts#trpc-api#procedure": {
131
+ "factory": "./src/trpc/procedure/generator",
132
+ "schema": "./src/trpc/procedure/schema.json",
133
+ "description": "Adds a procedure to a tRPC API"
134
+ }
135
+ },
136
+ ...
137
+ ```
138
+
139
+ ### Implement the Generator
140
+
141
+ To add a procedure to a tRPC API, we need to do two things:
142
+
143
+ 1. Create a TypeScript file for the new procedure
144
+ 2. Add the procedure to the router
145
+
146
+ #### Create the new Procedure
147
+
148
+ To create the TypeScript file for the new procedure, we'll use a utility called `generateFiles`. Using this, we can define an [EJS](https://ejs.co/) template which we can render in our generator with variables based on the options selected by the user.
149
+
150
+ First, we'll define the template in `packages/nx-plugin/src/trpc/procedure/files/procedures/__procedureNameKebabCase__.ts.template`:
151
+
152
+ ```ts title="files/procedures/__procedureNameKebabCase__.ts.template"
153
+ import { publicProcedure } from '../init.js';
154
+ import { z } from 'zod';
155
+
156
+ export const <%- procedureNameCamelCase %> = publicProcedure
157
+ .input(z.object({
158
+ // TODO: define input
159
+ }))
160
+ .output(z.object({
161
+ // TODO: define output
162
+ }))
163
+ .<%- procedureType %>(async ({ input, ctx }) => {
164
+ // TODO: implement!
165
+ return {};
166
+ });
167
+ ```
168
+
169
+ :::tip[Template Substitutions]
170
+ When `generateFiles` consumes the template, it will replace references to `__<variable>__` in file/directory names with the values it's provided, as well as stripping the `.template` from the file name.
171
+
172
+ The template content is [EJS](https://ejs.co/), where variables are referenced using the `<% ... %>` syntax.
173
+ :::
174
+
175
+ In the template, we referenced three variables:
176
+
177
+ - `procedureNameCamelCase`
178
+ - `procedureNameKebabCase`
179
+ - `procedureType`
180
+
181
+ So we'll need to make sure we pass those to `generateFiles`, as well as the directory to generate files into, namely the location of source files (i.e. `sourceRoot`) for the tRPC project the user selected as input for the generator, which we can extract from the project configuration.
182
+
183
+ Let's update the generator to do that:
184
+
185
+ ```ts title="procedure/generator.ts" {8-19}
186
+ import {
187
+ generateFiles,
188
+ joinPathFragments,
189
+ readProjectConfiguration,
190
+ type Tree,
191
+ } from '@nx/devkit';
192
+ import type { TrpcProcedureSchema } from './schema.js';
193
+ import { formatFilesInSubtree } from '../../utils/format';
194
+ import camelCase from 'lodash.camelcase';
195
+ import kebabCase from 'lodash.kebabcase';
196
+
197
+ export const trpcProcedureGenerator = async (
198
+ tree: Tree,
199
+ options: TrpcProcedureSchema,
200
+ ) => {
201
+ const projectConfig = readProjectConfiguration(tree, options.project);
202
+
203
+ const procedureNameCamelCase = camelCase(options.procedure);
204
+ const procedureNameKebabCase = kebabCase(options.procedure);
205
+
206
+ generateFiles(
207
+ tree,
208
+ joinPathFragments(import.meta.dirname, 'files'),
209
+ projectConfig.sourceRoot,
210
+ {
211
+ procedureNameCamelCase,
212
+ procedureNameKebabCase,
213
+ procedureType: options.type,
214
+ },
215
+ );
216
+
217
+ await formatFilesInSubtree(tree);
218
+ };
219
+
220
+ export default trpcProcedureGenerator;
221
+ ```
222
+
223
+ :::tip[File Formatting]
224
+ We also called `formatFilesInSubtree` at the end of the generator, which ensures that any files we create or modify are formatted according to the workspace's [Biome](https://biomejs.dev/) configuration.
225
+ :::
226
+
227
+ #### Add the Procedure to the Router
228
+
229
+ Next, we want the generator to hook up the new procedure to the router. This means reading and updating the user's source code!
230
+
231
+ We use [GritQL](https://docs.grit.io/) to declaratively search and transform source code. The `addDestructuredImport` helper adds named imports, and `applyGritQL` applies a GritQL pattern to add the procedure to the router's object literal.
232
+
233
+ ```ts title="procedure/generator.ts" {6, 23-36}
234
+ import {
235
+ generateFiles,
236
+ joinPathFragments,
237
+ readProjectConfiguration,
238
+ type Tree,
239
+ } from '@nx/devkit';
240
+ import type { TrpcProcedureSchema } from './schema.js';
241
+ import { formatFilesInSubtree } from '../../utils/format';
242
+ import camelCase from 'lodash.camelcase';
243
+ import kebabCase from 'lodash.kebabcase';
244
+ import { addDestructuredImport, applyGritQL } from '../../utils/ast';
245
+
246
+ export const trpcProcedureGenerator = async (
247
+ tree: Tree,
248
+ options: TrpcProcedureSchema,
249
+ ) => {
250
+ const projectConfig = readProjectConfiguration(tree, options.project);
251
+
252
+ const procedureNameCamelCase = camelCase(options.procedure);
253
+ const procedureNameKebabCase = kebabCase(options.procedure);
254
+
255
+ generateFiles(
256
+ tree,
257
+ joinPathFragments(import.meta.dirname, 'files'),
258
+ projectConfig.sourceRoot,
259
+ {
260
+ procedureNameCamelCase,
261
+ procedureNameKebabCase,
262
+ procedureType: options.type,
263
+ },
264
+ );
265
+
266
+ const routerPath = joinPathFragments(projectConfig.sourceRoot, 'router.ts');
267
+
268
+ await addDestructuredImport(
269
+ tree,
270
+ routerPath,
271
+ [procedureNameCamelCase],
272
+ `./procedures/${procedureNameKebabCase}.js`,
273
+ );
274
+
275
+ await applyGritQL(
276
+ tree,
277
+ routerPath,
278
+ `\`router({ $props })\` => \`router({ $props, ${procedureNameCamelCase} })\` where { $props <: not contains \`${procedureNameCamelCase}\` }`,
279
+ );
280
+
281
+ await formatFilesInSubtree(tree);
282
+ };
283
+
284
+ export default trpcProcedureGenerator;
285
+ ```
286
+
287
+ :::tip[GritQL Patterns]
288
+ In the above code snippet, `applyGritQL` uses a [GritQL](https://docs.grit.io/) pattern to find the `router({ ... })` call and add the new procedure to its object literal. The `where` clause ensures idempotency — if the procedure is already present, no change is made.
289
+
290
+ You can try out GritQL patterns in the [GritQL Playground](https://docs.grit.io/playground), or install the GritQL CLI to test patterns locally:
291
+
292
+ ```bash
293
+ npm install -g @getgrit/cli
294
+ grit apply '`router({ $props })` => `router({ $props, myProcedure })`' --dry-run
295
+ ```
296
+ :::
297
+
298
+ Now that we've implemented the generator, let's compile it to make sure it's available for us to test it out in our dungeon adventure project.
299
+
300
+ ```bash
301
+ pnpm nx compile @aws/nx-plugin
302
+ ```
303
+
304
+ ### Testing the Generator
305
+
306
+ To test the generator, we'll link our local Nx Plugin for AWS to an existing codebase.
307
+
308
+ #### Create a Test Project with a tRPC API
309
+
310
+ :::note[Using Your Own Project]
311
+ If you have completed the <Link path="get_started/tutorials/dungeon_game/overview">dungeon adventure tutorial</Link>, or already have another existing Nx workspace which uses a tRPC API, you can skip this step.
312
+ :::
313
+
314
+ In a separate directory, create a new test workspace:
315
+
316
+ <CreateNxWorkspaceCommand workspace="trpc-generator-test" />
317
+
318
+ Next, let's generate a tRPC API to add the procedure to:
319
+
320
+ <RunGenerator generator="ts#api" requiredParameters={{name:"test-api", framework:"trpc"}} noInteractive />
321
+
322
+ #### Link our local Nx Plugin for AWS
323
+
324
+ In your codebase, let's link our local `@aws/nx-plugin`:
325
+
326
+ <LinkCommand
327
+ dependency="@aws/nx-plugin"
328
+ dependencyPath="path/to/nx-plugin-for-aws/dist/packages/nx-plugin"
329
+ projectPath="path/to/trpc-generator-test"
330
+ />
331
+
332
+ :::note[Compiled Plugin Path]
333
+ Notice above we linked to the compiled plugin in `dist/packages/nx-plugin` rather than the source code.
334
+ :::
335
+
336
+ #### Run the new Generator
337
+
338
+ Let's try the new generator:
339
+
340
+ <RunGenerator generator="ts#trpc-api#procedure" />
341
+
342
+ :::note[Generator Not Showing]
343
+ If you don't see the new generator in the list in VSCode, you might need to refresh the Nx workspace:
344
+
345
+ <NxCommands commands={['reset']} />
346
+ :::
347
+
348
+ If successful, we should have generated a new procedure and added the procedure to our router in `router.ts`.
349
+
350
+ ### Exercises
351
+
352
+ If you've got this far and still have some time to experiment with Nx generators, here are some suggestions of features to add to the procedure generator:
353
+
354
+ #### 1. Nested Operations
355
+
356
+ Try updating the generator to support nested routers by:
357
+
358
+ - Accepting dot notation for the `procedure` input (e.g. `games.query`)
359
+ - Generating a procedure with a name based on reversed dot notation (e.g. `queryGames`)
360
+ - Adding the appropriate nested router (or updating it if it already exists!)
361
+
362
+ #### 2. Validation
363
+
364
+ Our generator should defend against potential issues, such as a user selecting a `project` which isn't a tRPC API. Take a look at the `connection` generator for an example of this.
365
+
366
+ #### 3. Unit Tests
367
+
368
+ Write some unit tests for the generator. These are quite straightforward to implement, and most follow the general flow:
369
+
370
+ 1. Create an empty workspace tree using `createTreeUsingTsSolutionSetup()`
371
+ 2. Add any files that should already exist in the tree (e.g. `project.json` and `src/router.ts` for a tRPC backend)
372
+ 3. Run the generator under test
373
+ 4. Validate the expected changes are made to the tree
374
+
375
+ #### 4. End to End Tests
376
+
377
+ We have a suite of "smoke tests" which run generators in a fresh workspace and make sure that everything builds. At a minimum, your new generator should be added to the generator matrix in `e2e/src/smoke-tests/generator-matrix.ts` so that it's exercised by the smoke tests.
378
+
379
+ Note that the generator matrix only runs the generators and builds the workspace — it does not instantiate any infrastructure. For generators which deploy infrastructure, consider extending the deployment e2e tests (`e2e/src/smoke-tests/cdk-deploy.spec.ts` and `terraform-deploy.spec.ts`) to actually deploy your resources (via `cdk deploy` / `terraform apply`), then add an assertion to `deploy-invocations.ts` that invokes the deployed resource and verifies it behaves as expected.
380
+
381
+ If your generator vends a local development server, consider adding it to the local development e2e test (`e2e/src/smoke-tests/local-dev.spec.ts`), which starts the `dev` target and exercises the running server.
382
+
383
+ ### General Guidance to Accelerate Contributing
384
+
385
+ This section contains some general guidance which can help when working on the Nx Plugin for AWS.
386
+
387
+ #### Work backwards from a real project
388
+
389
+ A useful way to build new generators or add features/fixes to an existing generator is to _build it for real first_. This way you can validate your ideas and iterate quickly to achieve the functionality you need. After you've settled on the desired outcome, you can then update the generator.
390
+
391
+ In practice, this process might look like:
392
+
393
+ <Steps>
394
+
395
+ 1. Create a new workspace
396
+
397
+ <CreateNxWorkspaceCommand workspace="my-project" />
398
+
399
+ 1. Run any generators that may be prerequisites to your new generator/feature/fix
400
+ 1. Commit your changes (`git commit`)
401
+ 1. Make your desired changes and test them as needed
402
+ 1. Use the `git diff` of your changes to inform what changes should be made to the Nx Plugin for AWS
403
+ 1. Perform one final end to end test (linking your `@aws/nx-plugin`) to ensure your generator vends the changes you need
404
+
405
+ </Steps>