Skip to content
Open
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ updates:
interval: 'monthly'
open-pull-requests-limit: 5
commit-message:
prefix: 'TEMPLATE-UPDATE'
prefix: 'NX-CDK-TEMPLATE'
groups:
minor-and-patch:
applies-to: version-updates
Expand All @@ -35,7 +35,7 @@ updates:
interval: 'monthly'
open-pull-requests-limit: 5
commit-message:
prefix: 'TEMPLATE-UPDATE'
prefix: 'NX-APPBUILDER-TEMPLATE'
groups:
minor-and-patch:
applies-to: version-updates
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ jobs:
- name: 🚀 Publish versioned packages
env:
PUBLISH_OPTIONS: ${{ inputs.publish-options }}
run: npx nx release publish --verbose $PUBLISH_OPTIONS
run: npx nx release publish --verbose --access public $PUBLISH_OPTIONS
shell: bash
16 changes: 16 additions & 0 deletions .nx/version-plans/version-plan-1777693034146.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
nx-cdk: minor
---

### New: remove-service generator

- Added a new generator to remove services from an NX CDK workspace.

### Improved: service generator

- Extracted shared constants (`SERVICES_FOLDER`, `MAIN_APPLICATION_NAME`) into a dedicated constants module.

### Infra:

- Added `--access public` flag to the nx release publish command in publish workflow.
- Renamed prefix from TEMPLATE-UPDATE to NX-CDK-TEMPLATE & NX-APPBUILDER-TEMPLATE in dependabot.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,19 @@ For more details, see [tools/generators/README.md](/tools/generators/README.md).

Each of the packages in the monorepo have separate versioning and independent npm releases. To perform a release of one or more packages we use [Version Plans](https://nx.dev/recipes/nx-release/file-based-versioning-version-plans) to define the type of updates and provide change log. Nx will then detect the version plans and automatically update version numbers appropriately, as well as perform builds and deployments separately in the pipeline if a version plan is detected.

## Step-by-Step Guide
## Important - First publish must be done manually

The first time a new package is published to `npm`, it must be published manually by a `maintainer`. Subsequent releases are then handled automatically by the release workflow. Contact with `DevOps` guild if you are not a maintainer of `@aligent/` on npm.

Our release workflow uses [OIDC trusted publishing](https://docs.npmjs.com/trusted-publishers). OIDC can only publish new versions of packages that already exist on npm — it cannot create a brand new package. The package name has to be registered on the npm registry before automated releases can take over.

To bootstrap a new package:

- Build the package locally: `npx nx build <package-name>`
- From the package directory, log in to npm (npm login) with maintainer credentials and publish: `npm publish --access public`
- Once the package exists on npm, future versions will be released automatically by the workflow.

## Step-by-Step Guide for subsequence releases

1. Start by creating a new `releases/*` branch from the latest `main` branch.

Expand Down
61 changes: 55 additions & 6 deletions packages/nx-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,17 @@ The service generator creates a new CDK service within the `services/` folder of

```bash
yarn nx g @aligent/nx-cdk:service <service-name>

# Or simply (since the generator name is unique):
yarn nx g service <service-name>
```

#### Options

| Option | Type | Required | Description |
| ------ | ------ | -------- | ------------------------------------------------------------------------- |
| `name` | string | Yes | The name of the service (cannot contain 'Stack' or 'Service' in the name) |
| Option | Type | Required | Default | Description |
| --------- | ------- | -------- | ------- | ------------------------------------------------------------------------- |
| `name` | string | Yes | - | The name of the service (cannot contain 'Stack' or 'Service' in the name) |
| `example` | boolean | No | `false` | Generate example code with sample resources |

#### What it creates

Expand All @@ -89,11 +93,56 @@ The service generator creates a new service in `services/<service-name>/` with:
#### Example

```bash
# Create a new service named "user-management"
# Create a new service named "user-management" (short form)
yarn nx g service user-management

# Or with the full plugin prefix
yarn nx g @aligent/nx-cdk:service user-management
```

### Remove Service Generator

The remove-service generator cleanly removes a service and all of its references from the project. It reverses the changes made by the service generator, ensuring no dangling imports or references are left behind.

#### Usage

```bash
yarn nx g @aligent/nx-cdk:remove-service <service-name>

# Or simply (since the generator name is unique):
yarn nx g remove-service <service-name>
```

#### Options

| Option | Type | Required | Default | Description |
| ------------- | ------- | -------- | ------- | ----------------------------------------------- |
| `name` | string | Yes | - | The name of the service to remove |
| `forceRemove` | boolean | No | `false` | Skip dependency check when removing the project |

#### What it does

The remove generator performs the following cleanup:

- **Application updates**:
- Removes the service's import declaration from `application/lib/service-stacks.ts`
- Removes the stack instantiation from the `ApplicationStage` constructor

- **Root updates**:
- Removes the service from the root `tsconfig.json` references
- Removes the service from the root `package.json` workspaces (if present)

- **Service files**:
- Deletes the entire `services/<service-name>/` directory

#### Example

```bash
# Remove the "user-management" service (short form)
yarn nx g remove-service user-management

# Create a new service named "payment-processing"
yarn nx g @aligent/nx-cdk:service payment-processing
# Or with the full plugin prefix
yarn nx g @aligent/nx-cdk:remove-service user-management
```

## Project Structure
Expand Down
5 changes: 5 additions & 0 deletions packages/nx-cdk/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"factory": "./src/generators/service/generator",
"schema": "./src/generators/service/schema.json",
"description": "Generate a new service"
},
"remove-service": {
"factory": "./src/generators/remove-service/generator",
"schema": "./src/generators/remove-service/schema.json",
"description": "Remove a service and clean up its references"
}
}
}
7 changes: 7 additions & 0 deletions packages/nx-cdk/src/generators/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ProjectType = 'application' | 'service';

export const MAIN_APPLICATION_FOLDER = 'application';
export const MAIN_APPLICATION_NAME = 'application';

export const SERVICES_FOLDER = 'services';
export const SERVICES_SCOPE = '@services';
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@aligent/cdk-aspects": "^0.5.5",
"@aligent/cdk-nodejs-function-from-entry": "^0.2.1",
"@aligent/cdk-step-function-from-file": "^0.5.1",
"@aligent/nx-openapi": "^2.2.1",
"@aligent/nx-openapi": "^2.1.1",
Comment thread
aligent-lturner marked this conversation as resolved.
"@aligent/ts-code-standards": "^4.2.1",
"@nx/eslint": "22.1.3",
"@nx/eslint-plugin": "22.1.3",
Expand Down
9 changes: 7 additions & 2 deletions packages/nx-cdk/src/generators/helpers/configs/nxJson.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NxJsonConfiguration } from '@nx/devkit';
import { SERVICES_SCOPE } from '../../constants';

export const NX_JSON: NxJsonConfiguration & { $schema: string } = {
$schema: './node_modules/nx/schemas/nx-schema.json',
Expand Down Expand Up @@ -36,7 +37,11 @@ export const NX_JSON: NxJsonConfiguration & { $schema: string } = {
configurations: { coverage: { coverage: true } },
},
typecheck: { cache: true, inputs: ['default', '^production'] },
cdk: { dependsOn: [{ target: 'build', params: 'forward', projects: '@services/*' }] },
pg: { dependsOn: [{ target: 'build', params: 'forward', projects: '@services/*' }] },
cdk: {
dependsOn: [{ target: 'build', params: 'forward', projects: `${SERVICES_SCOPE}/*` }],
},
pg: {
dependsOn: [{ target: 'build', params: 'forward', projects: `${SERVICES_SCOPE}/*` }],
},
},
} as const;
89 changes: 86 additions & 3 deletions packages/nx-cdk/src/generators/helpers/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* v8 ignore start */
import { readJsonFile, readProjectConfiguration, Tree } from '@nx/devkit';
import { readJsonFile, readProjectConfiguration, Tree, updateJson } from '@nx/devkit';
import { join } from 'path';
import { InMemoryFileSystemHost, Project } from 'ts-morph';
import { ProjectType, SERVICES_SCOPE } from '../constants';
import { TS_CONFIG_JSON, TS_CONFIG_LIB_JSON, TS_CONFIG_SPEC_JSON } from './configs/tsConfigs';

interface PackageJsonInput {
Expand Down Expand Up @@ -68,7 +69,7 @@ export function constructPackageJsonFile(input: PackageJsonInput) {
return packageJson;
}

export function constructProjectTsConfigFiles(type: 'application' | 'service') {
export function constructProjectTsConfigFiles(type: ProjectType) {
const tsConfig = { ...TS_CONFIG_JSON };
if (type === 'service') {
tsConfig.references = [{ path: './tsconfig.lib.json' }, { path: './tsconfig.spec.json' }];
Expand Down Expand Up @@ -152,7 +153,7 @@ export function addServiceStackToMainApplication(
}

stackSource.addImportDeclaration({
moduleSpecifier: `@services/${service.name}`,
moduleSpecifier: `${SERVICES_SCOPE}/${service.name}`,
namedImports: [service.constant, service.stack],
});

Expand All @@ -166,6 +167,88 @@ export function addServiceStackToMainApplication(
tree.write(stacksRelativePath, stackSource.getFullText());
}

/**
* Removes a service stack registration from the main CDK application's ApplicationStage.
*
* This function modifies the service-stacks.ts file by:
* 1. Removing import statements that reference the service's module specifier
* 2. Removing statements in the ApplicationStage constructor that reference the service's stack class
*
* @param tree - The Nx virtual file system tree
* @param serviceName - The name of the service (e.g., "companies")
* @param projectName - The name of the main application project
*/
export function removeServiceFromMainApplication(
tree: Tree,
serviceName: string,
projectName: string
) {
const application = readProjectConfiguration(tree, projectName);

if (application.root.includes('..')) {
throw new Error('Invalid application root path');
}

const stacksRelativePath = join(application.root, 'lib/service-stacks.ts');

if (!tree.exists(stacksRelativePath)) {
console.log('Service Stacks does not exist, skipping service stacks cleanup.');
return;
}

const content = tree.read(stacksRelativePath, 'utf-8');

if (content === null) {
throw new Error(`Failed to read file: ${stacksRelativePath}`);
}

const fs = new InMemoryFileSystemHost();
fs.writeFileSync(stacksRelativePath, content);

const project = new Project({ fileSystem: fs });
const stackSource = project.addSourceFileAtPath(stacksRelativePath);

const imports = stackSource.getImportDeclarations();
for (const importDecl of imports) {
if (importDecl.getModuleSpecifierValue() === `${SERVICES_SCOPE}/${serviceName}`) {
importDecl.remove();
}
}

const nameParts = splitInputName(serviceName);
const stackClassName = `${nameParts.join('')}Stack`;

const applicationStage = stackSource.getClass('ApplicationStage');
if (applicationStage) {
const stageConstructor = applicationStage.getConstructors()[0];
if (stageConstructor) {
const statements = stageConstructor.getStatements();
for (const statement of statements) {
if (statement.getText().includes(`new ${stackClassName}(`)) {
statement.remove();
}
}
}
}

tree.write(stacksRelativePath, stackSource.getFullText());
}

/**
* Removes a project reference from the root tsconfig.json.
*
* @param tree - The Nx virtual file system tree
* @param referencePath - The path to remove from the references array
*/
export function removeTsConfigReference(tree: Tree, referencePath: string) {
updateJson(tree, 'tsconfig.json', json => {
json.references = (json.references ?? []).filter(
(r: { path: string }) => r.path !== referencePath
);
return json;
});
}

/**
* Splits a kebab-case name into an array of capitalized parts.
*
Expand Down
5 changes: 3 additions & 2 deletions packages/nx-cdk/src/generators/preset/preset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { formatFiles, generateFiles, Tree, updateNxJson, writeJson } from '@nx/devkit';
import { join } from 'path';
import { MAIN_APPLICATION_FOLDER, MAIN_APPLICATION_NAME } from '../constants';
import { NX_JSON } from '../helpers/configs/nxJson';
import {
constructPackageJsonFile,
Expand Down Expand Up @@ -40,8 +41,8 @@ export async function presetGenerator(tree: Tree, options: PresetGeneratorSchema
writeJson(tree, 'package.json', packageJson);

// Generate application's tsconfigs
const { tsConfig } = constructProjectTsConfigFiles('application');
writeJson(tree, 'application/tsconfig.json', tsConfig);
const { tsConfig } = constructProjectTsConfigFiles(MAIN_APPLICATION_NAME);
writeJson(tree, `${MAIN_APPLICATION_FOLDER}/tsconfig.json`, tsConfig);

await formatFiles(tree);
}
Expand Down
Loading
Loading