From 2b349ffb49f0830ead09030684cef4a6518543bf Mon Sep 17 00:00:00 2001 From: Caleb Ukle Date: Thu, 22 Jun 2023 16:18:55 -0500 Subject: [PATCH] feat(nest): add v10 migrations for tsconfig & CacheModule --- packages/nest/migrations.json | 6 + .../nestjs-10-updates.ts | 138 +++++++++++++ .../nestjs-10.spec.ts | 185 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10-updates.ts create mode 100644 packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10.spec.ts diff --git a/packages/nest/migrations.json b/packages/nest/migrations.json index 14ff4c76c94b91..9c354a934f3e46 100644 --- a/packages/nest/migrations.json +++ b/packages/nest/migrations.json @@ -11,6 +11,12 @@ "version": "16.0.0-beta.1", "description": "Replace @nrwl/nest with @nx/nest", "implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages" + }, + "update-16-4-0-support-nestjs-10": { + "cli": "nx", + "version": "16.4.0-beta.11", + "description": "Update TsConfig target to es2021 and CacheModule if being used. Read more at https://docs.nestjs.com/migration-guide", + "implementation": "./src/migrations/update-16-4-0-cache-manager/nestjs-10-updates" } }, "packageJsonUpdates": { diff --git a/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10-updates.ts b/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10-updates.ts new file mode 100644 index 00000000000000..2587a88ba4be06 --- /dev/null +++ b/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10-updates.ts @@ -0,0 +1,138 @@ +import { + Tree, + addDependenciesToPackageJson, + createProjectGraphAsync, + formatFiles, + getProjects, + updateJson, + visitNotIgnoredFiles, +} from '@nx/devkit'; +import { NormalizedWebpackExecutorOptions } from '@nx/webpack'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; +import { + ImportDeclaration, + VariableStatement, + ScriptTarget, + isVariableStatement, +} from 'typescript'; + +const JS_TS_FILE_MATCHER = /\.[jt]sx?$/; + +const importMatch = + ':matches(ImportDeclaration, VariableStatement):has(Identifier[name="CacheModule"], Identifier[name="CacheModule"]):has(StringLiteral[value="@nestjs/common"])'; + +export async function updateNestJs10(tree: Tree) { + const nestProjects = await getNestProejcts(); + if (nestProjects.length === 0) { + return; + } + + const projects = getProjects(tree); + + let installCacheModuleDeps = false; + forEachExecutorOptions( + tree, + '@nx/webpack:webpack', + (options, projectName) => { + if (!nestProjects.includes(projectName)) { + return; + } + + if (options.tsConfig && tree.exists(options.tsConfig)) { + updateTsConfigTarget(tree, options.tsConfig); + } + + const projectConfig = projects.get(projectName); + + visitNotIgnoredFiles(tree, projectConfig.root, (filePath) => { + if (!JS_TS_FILE_MATCHER.test(filePath)) { + return; + } + + installCacheModuleDeps = + updateCacheManagerImport(tree, filePath) || installCacheModuleDeps; + }); + } + ); + + await formatFiles(tree); + + return installCacheModuleDeps + ? addDependenciesToPackageJson( + tree, + { + '@nestjs/cache-manager': '^2.0.0', + 'cache-manager': '^5.2.3', + }, + {} + ) + : () => {}; +} + +async function getNestProejcts(): Promise { + const projectGraph = await createProjectGraphAsync(); + + return Object.entries(projectGraph.dependencies) + .filter(([node, dep]) => + dep.some( + ({ target }) => + !projectGraph.externalNodes?.[node] && target === 'npm:@nestjs/common' + ) + ) + .map(([projectName]) => projectName); +} + +// change import { CacheModule } from '@nestjs/common'; +// to import { CacheModule } from '@nestjs/cache-manager'; +export function updateCacheManagerImport( + tree: Tree, + filePath: string +): boolean { + const content = tree.read(filePath, 'utf-8'); + + const updated = tsquery.replace( + content, + importMatch, + + (node: ImportDeclaration | VariableStatement) => { + const text = node.getText(); + return `${text.replace('CacheModule', '')}\n${ + isVariableStatement(node) + ? "const { CacheModule } = require('@nestjs/cache-manager')" + : "import { CacheModule } from '@nestjs/cache-manager';" + }`; + } + ); + + if (updated !== content) { + tree.write(filePath, updated); + return true; + } +} + +export function updateTsConfigTarget(tree: Tree, tsConfigPath: string) { + updateJson(tree, tsConfigPath, (json) => { + if (!json.compilerOptions.target) { + return; + } + + const normalizedTargetName = json.compilerOptions.target.toUpperCase(); + // es6 isn't apart of the ScriptTarget enum but is a valid tsconfig target in json file + const existingTarget = + normalizedTargetName === 'ES6' + ? ScriptTarget.ES2015 + : (ScriptTarget[normalizedTargetName] as unknown as ScriptTarget); + + if ( + existingTarget < ScriptTarget.ES2021 || + json.compilerOptions.target === 'ES6' + ) { + json.compilerOptions.target = 'es2021'; + } + + return json; + }); +} + +export default updateNestJs10; diff --git a/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10.spec.ts b/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10.spec.ts new file mode 100644 index 00000000000000..ab3ed6f69510e5 --- /dev/null +++ b/packages/nest/src/migrations/update-16-4-0-cache-manager/nestjs-10.spec.ts @@ -0,0 +1,185 @@ +import { + ProjectConfiguration, + ProjectGraph, + Tree, + addProjectConfiguration, + readJson, +} from '@nx/devkit'; +import { + updateNestJs10, + updateCacheManagerImport, + updateTsConfigTarget, +} from './nestjs-10-updates'; +import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: () => Promise.resolve(projectGraph), +})); +describe('nestjs 10 migration changes', () => { + let tree: Tree; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + jest.resetAllMocks(); + }); + + it('should update nestjs project', async () => { + tree.write( + 'apps/app1/main.ts', + ` +/** + * This is not a production server yet! + * This is only a minimal backend to get started. + */ + +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; +import { CacheModule } from '@nestjs/common'; +import { AppModule } from './app/app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const globalPrefix = 'api'; + app.setGlobalPrefix(globalPrefix); + const port = process.env.PORT || 3000; + await app.listen(port); + Logger.log('🚀 Application is running on: http://localhost:' + port + '/' + globalPrefix); +} + +bootstrap(); +` + ); + + tree.write( + 'apps/app1/tsconfig.app.json', + JSON.stringify({ + extends: './tsconfig.json', + compilerOptions: { + outDir: '../../dist/out-tsc', + module: 'commonjs', + types: ['node'], + emitDecoratorMetadata: true, + target: 'es2015', + }, + exclude: ['jest.config.ts', 'src/**/*.spec.ts', 'src/**/*.test.ts'], + include: ['src/**/*.ts'], + }) + ); + addProject( + tree, + 'app1', + { + root: 'apps/app1', + targets: { + build: { + executor: '@nx/webpack:webpack', + options: { + tsConfig: 'apps/app1/tsconfig.app.json', + }, + }, + }, + }, + ['npm:@nestjs/common'] + ); + + await updateNestJs10(tree); + + expect(readJson(tree, 'package.json').dependencies).toMatchInlineSnapshot(` + { + "@nestjs/cache-manager": "^2.0.0", + "cache-manager": "^5.2.3", + } + `); + expect( + readJson(tree, 'apps/app1/tsconfig.app.json').compilerOptions.target + ).toEqual('es2021'); + expect(tree.read('apps/app1/main.ts', 'utf-8')).toContain( + "import { CacheModule } from '@nestjs/cache-manager';" + ); + }); + + it('should update cache module import', () => { + tree.write( + 'main.ts', + ` +import { CacheModule } from '@nestjs/common'; +const { CacheModule } = require('@nestjs/common'); +` + ); + const actual = updateCacheManagerImport(tree, 'main.ts'); + + expect(tree.read('main.ts', 'utf-8')).toMatchInlineSnapshot(` + " + import { } from '@nestjs/common'; + import { CacheModule } from '@nestjs/cache-manager'; + const { } = require('@nestjs/common'); + const { CacheModule } = require('@nestjs/cache-manager') + " + `); + expect(actual).toBe(true); + }); + + it('should NOT update cache module imports', () => { + tree.write( + 'main.ts', + ` +import { AnotherModule } from '@nestjs/common'; +const { AnotherModule } = require('@nestjs/common'); +` + ); + const actual = updateCacheManagerImport(tree, 'main.ts'); + + expect(tree.read('main.ts', 'utf-8')).toMatchInlineSnapshot(` + " + import { AnotherModule } from '@nestjs/common'; + const { AnotherModule } = require('@nestjs/common'); + " + `); + expect(actual).toBeUndefined(); + }); + + it('should update script target', () => { + tree.write( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { target: 'es6' } }) + ); + updateTsConfigTarget(tree, 'tsconfig.json'); + expect(readJson(tree, 'tsconfig.json').compilerOptions.target).toBe( + 'es2021' + ); + }); + + it('should NOT update script if over es2021', () => { + tree.write( + 'tsconfig.json', + JSON.stringify({ compilerOptions: { target: 'es2022' } }) + ); + updateTsConfigTarget(tree, 'tsconfig.json'); + expect(readJson(tree, 'tsconfig.json').compilerOptions.target).toBe( + 'es2022' + ); + }); +}); + +function addProject( + tree: Tree, + projectName: string, + config: ProjectConfiguration, + dependencies: string[] +): void { + projectGraph = { + dependencies: { + [projectName]: dependencies.map((d) => ({ + source: projectName, + target: d, + type: 'static', + })), + }, + nodes: { + [projectName]: { data: config, name: projectName, type: 'app' }, + }, + }; + addProjectConfiguration(tree, projectName, config); +}