feat(nest): add v10 migrations for tsconfig & CacheModule (#17741)

This commit is contained in:
Caleb Ukle 2023-06-23 09:50:17 -05:00 committed by GitHub
parent 5fa6e487eb
commit 62651a52dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 384 additions and 1 deletions

View File

@ -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.16",
"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": {

View File

@ -35,7 +35,8 @@
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"@nx/linter": "file:../linter",
"@nx/node": "file:../node"
"@nx/node": "file:../node",
"@phenomnomnominal/tsquery": "~5.0.1"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,134 @@
import {
Tree,
addDependenciesToPackageJson,
createProjectGraphAsync,
formatFiles,
getProjects,
joinPathFragments,
updateJson,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
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;
}
let installCacheModuleDeps = false;
const projects = getProjects(tree);
for (const projectName of nestProjects) {
const projectConfig = projects.get(projectName);
const tsConfig =
projectConfig.targets?.build?.options?.tsConfig ??
joinPathFragments(
projectConfig.root,
projectConfig.projectType === 'application'
? 'tsconfig.app.json'
: 'tsconfig.lib.json'
);
if (tree.exists(tsConfig)) {
updateTsConfigTarget(tree, tsConfig);
}
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<string[]> {
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 = 'es2021';
}
return json;
});
}
export default updateNestJs10;

View File

@ -0,0 +1,242 @@
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 work with non buildable lib', async () => {
tree.write(
'libs/lib1/src/lib/lib1.module.ts',
`
import { Module, CacheModule } from '@nestjs/common';
@Module({
controllers: [],
providers: [],
exports: [],
imports: [CacheModule.register()],
})
export class LibOneModule {}
`
);
tree.write(
'libs/lib1/tsconfig.lib.json',
JSON.stringify({
extends: './tsconfig.json',
compilerOptions: {
outDir: '../../dist/out-tsc',
module: 'commonjs',
types: ['node'],
emitDecoratorMetadata: true,
target: 'es6',
},
exclude: ['jest.config.ts', 'src/**/*.spec.ts', 'src/**/*.test.ts'],
include: ['src/**/*.ts'],
})
);
addProject(
tree,
'app1',
{
root: 'libs/lib1',
targets: {},
},
['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, 'libs/lib1/tsconfig.lib.json').compilerOptions.target
).toEqual('es2021');
expect(tree.read('libs/lib1/src/lib/lib1.module.ts', 'utf-8')).toContain(
"import { CacheModule } from '@nestjs/cache-manager';"
);
});
it('should update cache module import', () => {
tree.write(
'main.ts',
`
import { Module, CacheModule } from '@nestjs/common';
const { Module, CacheModule } = require('@nestjs/common');
`
);
const actual = updateCacheManagerImport(tree, 'main.ts');
expect(tree.read('main.ts', 'utf-8')).toMatchInlineSnapshot(`
"
import { Module, } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
const { Module, } = 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);
}