Skip to content

Commit

Permalink
feat(nest): add v10 migrations for tsconfig & CacheModule
Browse files Browse the repository at this point in the history
  • Loading branch information
barbados-clemens committed Jun 22, 2023
1 parent 16b9b20 commit 2b349ff
Show file tree
Hide file tree
Showing 3 changed files with 329 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/nest/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NormalizedWebpackExecutorOptions>(
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<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 === 'ES6'
) {
json.compilerOptions.target = 'es2021';
}

return json;
});
}

export default updateNestJs10;
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit 2b349ff

Please sign in to comment.