import * as path from 'path'; import { checkFilesDoNotExist, checkFilesExist, cleanupProject, createFile, getSelectedPackageManager, newProject, readFile, readJson, runCLI, runCreateWorkspace, uniq, updateFile, updateJson, } from '@nx/e2e/utils'; import * as ts from 'typescript'; /** * Importing this helper from @typescript-eslint/type-utils to ensure * compatibility with TS < 4.8 due to the API change in TS4.8. * This helper allows for support of TS <= 4.8. */ import { getModifiers } from '@typescript-eslint/type-utils'; describe('Linter', () => { describe('Integrated', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); let projScope; beforeAll(() => { projScope = newProject(); runCLI(`generate @nx/react:app ${myapp} --tags=validtag`); runCLI(`generate @nx/js:lib ${mylib}`); }); afterAll(() => cleanupProject()); describe('linting errors', () => { let defaultEslintrc; beforeAll(() => { updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); defaultEslintrc = readJson('.eslintrc.json'); }); afterEach(() => { updateFile('.eslintrc.json', JSON.stringify(defaultEslintrc, null, 2)); }); it('should check for linting errors', () => { // create faulty file updateFile(`apps/${myapp}/src/main.ts`, `console.log("should fail");`); const eslintrc = readJson('.eslintrc.json'); // set the eslint rules to error eslintrc.overrides.forEach((override) => { if (override.files.includes('*.ts')) { override.rules['no-console'] = 'error'; } }); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); // 1. linting should error when rules are not followed let out = runCLI(`lint ${myapp}`, { silenceError: true }); expect(out).toContain('Unexpected console statement'); // 2. linting should not error when rules are not followed and the force flag is specified expect(() => runCLI(`lint ${myapp} --force`)).not.toThrow(); eslintrc.overrides.forEach((override) => { if (override.files.includes('*.ts')) { override.rules['no-console'] = undefined; } }); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); // 3. linting should not error when all rules are followed out = runCLI(`lint ${myapp}`, { silenceError: true }); expect(out).toContain('All files pass linting'); }, 1000000); it('should cache eslint with --cache', () => { function readCacheFile(cacheFile) { const cacheInfo = readFile(cacheFile); return process.platform === 'win32' ? cacheInfo.replace(/\\\\/g, '\\') : cacheInfo; } // should generate a default cache file expect(() => checkFilesExist(`.eslintcache`)).toThrow(); runCLI(`lint ${myapp} --cache`, { silenceError: true }); expect(() => checkFilesExist(`.eslintcache`)).not.toThrow(); expect(readCacheFile(`.eslintcache`)).toContain( path.normalize(`${myapp}/src/app/app.spec.tsx`) ); // should let you specify a cache file location expect(() => checkFilesExist(`my-cache`)).toThrow(); runCLI(`lint ${myapp} --cache --cache-location="my-cache"`, { silenceError: true, }); expect(() => checkFilesExist(`my-cache/${myapp}`)).not.toThrow(); expect(readCacheFile(`my-cache/${myapp}`)).toContain( path.normalize(`${myapp}/src/app/app.spec.tsx`) ); }); it('linting should generate an output file with a specific format', () => { const eslintrc = readJson('.eslintrc.json'); eslintrc.overrides.forEach((override) => { if (override.files.includes('*.ts')) { override.rules['no-console'] = 'error'; } }); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); const outputFile = 'a/b/c/lint-output.json'; expect(() => { checkFilesExist(outputFile); }).toThrow(); const stdout = runCLI( `lint ${myapp} --output-file="${outputFile}" --format=json`, { silenceError: true, } ); expect(stdout).not.toContain('Unexpected console statement'); expect(() => checkFilesExist(outputFile)).not.toThrow(); const outputContents = JSON.parse(readFile(outputFile)); const outputForApp: any = Object.values(outputContents).filter( (result: any) => result.filePath.includes(path.normalize(`${myapp}/src/main.ts`)) )[0]; expect(outputForApp.errorCount).toBe(1); expect(outputForApp.messages[0].ruleId).toBe('no-console'); expect(outputForApp.messages[0].message).toBe( 'Unexpected console statement.' ); }, 1000000); it('should support creating, testing and using workspace lint rules', () => { const messageId = 'e2eMessageId'; const libMethodName = 'getMessageId'; // add custom function updateFile( `libs/${mylib}/src/lib/${mylib}.ts`, `export const ${libMethodName} = (): '${messageId}' => '${messageId}';` ); // Generate a new rule (should also scaffold the required workspace project and tests) const newRuleName = 'e2e-test-rule-name'; runCLI(`generate @nx/linter:workspace-rule ${newRuleName}`); // Ensure that the unit tests for the new rule are runnable const unitTestsOutput = runCLI(`test eslint-rules`); expect(unitTestsOutput).toContain('Successfully ran target test'); // Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used const knownLintErrorMessage = 'e2e test known error message'; const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`; const newRuleGeneratedContents = readFile(newRulePath); const updatedRuleContents = updateGeneratedRuleImplementation( newRulePath, newRuleGeneratedContents, knownLintErrorMessage, messageId, libMethodName, `@${projScope}/${mylib}` ); updateFile(newRulePath, updatedRuleContents); const newRuleNameForUsage = `@nx/workspace/${newRuleName}`; // Add the new workspace rule to the lint config and run linting const eslintrc = readJson('.eslintrc.json'); eslintrc.overrides.forEach((override) => { if (override.files.includes('*.ts')) { override.rules[newRuleNameForUsage] = 'error'; } }); updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); const lintOutput = runCLI(`lint ${myapp} --verbose`, { silenceError: true, }); expect(lintOutput).toContain(newRuleNameForUsage); expect(lintOutput).toContain(knownLintErrorMessage); }, 1000000); it('lint plugin should ensure module boundaries', () => { const myapp2 = uniq('myapp2'); const lazylib = uniq('lazylib'); const invalidtaglib = uniq('invalidtaglib'); const validtaglib = uniq('validtaglib'); runCLI(`generate @nx/react:app ${myapp2}`); runCLI(`generate @nx/react:lib ${lazylib}`); runCLI(`generate @nx/js:lib ${invalidtaglib} --tags=invalidtag`); runCLI(`generate @nx/js:lib ${validtaglib} --tags=validtag`); const eslint = readJson('.eslintrc.json'); eslint.overrides[0].rules[ '@nx/enforce-module-boundaries' ][1].depConstraints = [ { sourceTag: 'validtag', onlyDependOnLibsWithTags: ['validtag'] }, ...eslint.overrides[0].rules['@nx/enforce-module-boundaries'][1] .depConstraints, ]; updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2)); const tsConfig = readJson('tsconfig.base.json'); /** * apps do not add themselves to the tsconfig file. * * Let's add it so that we can trigger the lint failure */ tsConfig.compilerOptions.paths[`@${projScope}/${myapp2}`] = [ `apps/${myapp2}/src/main.ts`, ]; tsConfig.compilerOptions.paths[`@secondScope/${lazylib}`] = tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; delete tsConfig.compilerOptions.paths[`@${projScope}/${lazylib}`]; updateFile('tsconfig.base.json', JSON.stringify(tsConfig, null, 2)); updateFile( `apps/${myapp}/src/main.ts`, ` import '../../../libs/${mylib}'; import '@secondScope/${lazylib}'; import '@${projScope}/${myapp2}'; import '@${projScope}/${invalidtaglib}'; import '@${projScope}/${validtaglib}'; const s = {loadChildren: '@secondScope/${lazylib}'}; ` ); const out = runCLI(`lint ${myapp}`, { silenceError: true }); expect(out).toContain( 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' ); expect(out).toContain('Imports of apps are forbidden'); expect(out).toContain( 'A project tagged with "validtag" can only depend on libs tagged with "validtag"' ); }, 1000000); it('should print the effective configuration for a file specified using --printConfig', () => { const eslint = readJson('.eslintrc.json'); eslint.overrides.push({ files: ['src/index.ts'], rules: { 'specific-rule': 'off', }, }); updateFile('.eslintrc.json', JSON.stringify(eslint, null, 2)); const out = runCLI(`lint ${myapp} --printConfig src/index.ts`, { silenceError: true, }); expect(out).toContain('"specific-rule": ['); }, 1000000); }); describe('workspace boundary rules', () => { const libA = uniq('tslib-a'); const libB = uniq('tslib-b'); const libC = uniq('tslib-c'); beforeAll(() => { runCLI(`generate @nx/js:lib ${libA}`); runCLI(`generate @nx/js:lib ${libB}`); runCLI(`generate @nx/js:lib ${libC}`); /** * create tslib-a structure */ createFile( `libs/${libA}/src/lib/tslib-a.ts`, ` export function libASayHi(): string { return 'hi there'; } export function libASayHello(): string { return 'Hi from tslib-a'; } ` ); createFile( `libs/${libA}/src/lib/some-non-exported-function.ts`, ` export function someNonPublicLibFunction() { return 'this function is exported, but not via the libs barrel file'; } export function someSelectivelyExportedFn() { return 'this fn is exported selectively in the barrel file'; } ` ); createFile( `libs/${libA}/src/index.ts`, ` export * from './lib/tslib-a'; export { someSelectivelyExportedFn } from './lib/some-non-exported-function'; ` ); /** * create tslib-b structure */ createFile( `libs/${libB}/src/index.ts`, ` export * from './lib/tslib-b'; ` ); createFile( `libs/${libB}/src/lib/tslib-b.ts`, ` import { libASayHi } from 'libs/${libA}/src/lib/tslib-a'; import { libASayHello } from '../../../${libA}/src/lib/tslib-a'; // import { someNonPublicLibFunction } from '../../../${libA}/src/lib/some-non-exported-function'; import { someSelectivelyExportedFn } from '../../../${libA}/src/lib/some-non-exported-function'; export function tslibB(): string { // someNonPublicLibFunction(); someSelectivelyExportedFn(); libASayHi(); libASayHello(); return 'hi there'; } ` ); /** * create tslib-c structure */ createFile( `libs/${libC}/src/index.ts`, ` export * from './lib/tslib-c'; export * from './lib/constant'; ` ); createFile( `libs/${libC}/src/lib/constant.ts`, ` export const SOME_CONSTANT = 'some constant value'; export const someFunc1 = () => 'hi'; export function someFunc2() { return 'hi2'; } ` ); createFile( `libs/${libC}/src/lib/tslib-c-another.ts`, ` import { tslibC, SOME_CONSTANT, someFunc1, someFunc2 } from '@${projScope}/${libC}'; export function someStuff() { someFunc1(); someFunc2(); tslibC(); console.log(SOME_CONSTANT); return 'hi'; } ` ); createFile( `libs/${libC}/src/lib/tslib-c.ts`, ` import { someFunc1, someFunc2, SOME_CONSTANT } from '@${projScope}/${libC}'; export function tslibC(): string { someFunc1(); someFunc2(); console.log(SOME_CONSTANT); return 'tslib-c'; } ` ); }); it('should fix noSelfCircularDependencies', () => { const stdout = runCLI(`lint ${libC}`, { silenceError: true, }); expect(stdout).toContain( 'Projects should use relative imports to import from other files within the same project' ); // fix them const fixedStout = runCLI(`lint ${libC} --fix`, { silenceError: true, }); expect(fixedStout).toContain( `Successfully ran target lint for project ${libC}` ); const fileContent = readFile(`libs/${libC}/src/lib/tslib-c-another.ts`); expect(fileContent).toContain(`import { tslibC } from './tslib-c';`); expect(fileContent).toContain( `import { SOME_CONSTANT, someFunc1, someFunc2 } from './constant';` ); const fileContentTslibC = readFile(`libs/${libC}/src/lib/tslib-c.ts`); expect(fileContentTslibC).toContain( `import { someFunc1, someFunc2, SOME_CONSTANT } from './constant';` ); }); it('should fix noRelativeOrAbsoluteImportsAcrossLibraries', () => { const stdout = runCLI(`lint ${libB}`, { silenceError: true, }); expect(stdout).toContain( 'Projects cannot be imported by a relative or absolute path, and must begin with a npm scope' ); // fix them const fixedStout = runCLI(`lint ${libB} --fix`, { silenceError: true, }); expect(fixedStout).toContain( `Successfully ran target lint for project ${libB}` ); const fileContent = readFile(`libs/${libB}/src/lib/tslib-b.ts`); expect(fileContent).toContain( `import { libASayHello } from '@${projScope}/${libA}';` ); expect(fileContent).toContain( `import { libASayHi } from '@${projScope}/${libA}';` ); expect(fileContent).toContain( `import { someSelectivelyExportedFn } from '@${projScope}/${libA}';` ); }); }); describe('dependency checks', () => { beforeAll(() => { updateJson(`libs/${mylib}/.eslintrc.json`, (json) => { json.overrides = [ ...json.overrides, { files: ['*.json'], parser: 'jsonc-eslint-parser', rules: { '@nx/dependency-checks': 'error', }, }, ]; return json; }); updateJson(`libs/${mylib}/project.json`, (json) => { json.targets.lint.options.lintFilePatterns = [ `libs/${mylib}/**/*.ts`, `libs/${mylib}/project.json`, `libs/${mylib}/package.json`, ]; return json; }); }); it('should report dependency check issues', () => { const rootPackageJson = readJson('package.json'); const nxVersion = rootPackageJson.devDependencies.nx; const tslibVersion = rootPackageJson.dependencies['tslib']; let out = runCLI(`lint ${mylib}`, { silenceError: true }); expect(out).toContain('All files pass linting'); // make an explict dependency to nx updateFile( `libs/${mylib}/src/lib/${mylib}.ts`, (content) => `import { names } from '@nx/devkit';\n\n` + content.replace(/return .*;/, `return names(${mylib}).className;`) ); // output should now report missing dependency out = runCLI(`lint ${mylib}`, { silenceError: true }); expect(out).toContain('they are missing'); expect(out).toContain('@nx/devkit'); // should fix the missing dependency issue out = runCLI(`lint ${mylib} --fix`, { silenceError: true }); expect(out).toContain( `Successfully ran target lint for project ${mylib}` ); const packageJson = readJson(`libs/${mylib}/package.json`); expect(packageJson).toMatchInlineSnapshot(` { "dependencies": { "@nx/devkit": "${nxVersion}", "tslib": "${tslibVersion}", }, "main": "./src/index.js", "name": "@proj/${mylib}", "type": "commonjs", "typings": "./src/index.d.ts", "version": "0.0.1", } `); // intentionally set the invalid version updateJson(`libs/${mylib}/package.json`, (json) => { json.dependencies['@nx/devkit'] = '100.0.0'; return json; }); out = runCLI(`lint ${mylib}`, { silenceError: true }); expect(out).toContain( 'version specifier does not contain the installed version of "@nx/devkit"' ); // should fix the version mismatch issue out = runCLI(`lint ${mylib} --fix`, { silenceError: true }); expect(out).toContain( `Successfully ran target lint for project ${mylib}` ); }); }); }); describe('Flat config', () => { const packageManager = getSelectedPackageManager() || 'pnpm'; afterEach(() => cleanupProject()); it('should convert integrated to flat config', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); runCreateWorkspace(myapp, { preset: 'react-monorepo', appName: myapp, style: 'css', packageManager, bundler: 'vite', e2eTestRunner: 'none', }); runCLI(`generate @nx/js:lib ${mylib}`); // migrate to flat structure runCLI(`generate @nx/linter:convert-to-flat-config`); checkFilesExist( 'eslint.config.js', `apps/${myapp}/eslint.config.js`, `libs/${mylib}/eslint.config.js` ); checkFilesDoNotExist( '.eslintrc.json', `apps/${myapp}/.eslintrc.json`, `libs/${mylib}/.eslintrc.json` ); const outFlat = runCLI(`affected -t lint`, { silenceError: true, }); expect(outFlat).toContain('All files pass linting'); }, 1000000); it('should convert standalone to flat config', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); runCreateWorkspace(myapp, { preset: 'react-standalone', appName: myapp, style: 'css', packageManager, bundler: 'vite', e2eTestRunner: 'none', }); runCLI(`generate @nx/js:lib ${mylib}`); // migrate to flat structure runCLI(`generate @nx/linter:convert-to-flat-config`); checkFilesExist( 'eslint.config.js', `${mylib}/eslint.config.js`, 'eslint.base.config.js' ); checkFilesDoNotExist( '.eslintrc.json', `${mylib}/.eslintrc.json`, '.eslintrc.base.json' ); const outFlat = runCLI(`affected -t lint`, { silenceError: true, }); expect(outFlat).toContain('All files pass linting'); }, 1000000); }); describe('Root projects migration', () => { beforeEach(() => newProject()); afterEach(() => cleanupProject()); function verifySuccessfulStandaloneSetup(myapp: string) { expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain( 'All files pass linting' ); expect(runCLI(`lint e2e`, { silenceError: true })).toContain( 'All files pass linting' ); expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); const rootEslint = readJson('.eslintrc.json'); const e2eEslint = readJson('e2e/.eslintrc.json'); // should directly refer to nx plugin expect(rootEslint.plugins).toEqual(['@nx']); expect(e2eEslint.plugins).toEqual(['@nx']); } function verifySuccessfulMigratedSetup(myapp: string, mylib: string) { expect(runCLI(`lint ${myapp}`, { silenceError: true })).toContain( 'All files pass linting' ); expect(runCLI(`lint e2e`, { silenceError: true })).toContain( 'All files pass linting' ); expect(runCLI(`lint ${mylib}`, { silenceError: true })).toContain( 'All files pass linting' ); expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); const rootEslint = readJson('.eslintrc.base.json'); const appEslint = readJson('.eslintrc.json'); const e2eEslint = readJson('e2e/.eslintrc.json'); const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); // should directly refer to nx plugin expect(rootEslint.plugins).toEqual(['@nx']); expect(appEslint.plugins).toBeUndefined(); expect(e2eEslint.plugins).toBeUndefined(); expect(libEslint.plugins).toBeUndefined(); // should extend base expect(appEslint.extends.slice(-1)).toEqual(['./.eslintrc.base.json']); expect(e2eEslint.extends.slice(-1)).toEqual(['../.eslintrc.base.json']); expect(libEslint.extends.slice(-1)).toEqual([ '../../.eslintrc.base.json', ]); } it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); runCLI(`generate @nx/react:app ${myapp} --rootProject=true`); verifySuccessfulStandaloneSetup(myapp); let appEslint = readJson('.eslintrc.json'); let e2eEslint = readJson('e2e/.eslintrc.json'); // should have plugin extends expect(appEslint.overrides[0].extends).toBeDefined(); expect(appEslint.overrides[1].extends).toBeDefined(); expect(e2eEslint.overrides[0].extends).toBeDefined(); runCLI(`generate @nx/js:lib ${mylib} --unitTestRunner=jest`); verifySuccessfulMigratedSetup(myapp, mylib); appEslint = readJson(`.eslintrc.json`); e2eEslint = readJson('e2e/.eslintrc.json'); // should have no plugin extends expect(appEslint.overrides[0].extends).toBeUndefined(); expect(appEslint.overrides[1].extends).toBeUndefined(); expect(e2eEslint.overrides[0].extends).toBeUndefined(); }); it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); runCLI( `generate @nx/angular:app ${myapp} --rootProject=true --no-interactive` ); verifySuccessfulStandaloneSetup(myapp); let appEslint = readJson('.eslintrc.json'); let e2eEslint = readJson('e2e/.eslintrc.json'); // should have plugin extends expect(appEslint.overrides[0].extends).toBeDefined(); expect(appEslint.overrides[1].extends).toBeDefined(); expect(e2eEslint.overrides[0].extends).toBeDefined(); runCLI(`generate @nx/js:lib ${mylib} --no-interactive`); verifySuccessfulMigratedSetup(myapp, mylib); appEslint = readJson(`.eslintrc.json`); e2eEslint = readJson('e2e/.eslintrc.json'); // should have no plugin extends expect(appEslint.overrides[0].extends).toEqual([ 'plugin:@nx/angular', 'plugin:@angular-eslint/template/process-inline-templates', ]); expect(e2eEslint.overrides[0].extends).toBeUndefined(); }); it('(Node standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); runCLI( `generate @nx/node:app ${myapp} --rootProject=true --no-interactive` ); verifySuccessfulStandaloneSetup(myapp); let appEslint = readJson('.eslintrc.json'); let e2eEslint = readJson('e2e/.eslintrc.json'); // should have plugin extends expect(appEslint.overrides[0].extends).toBeDefined(); expect(appEslint.overrides[1].extends).toBeDefined(); expect(e2eEslint.overrides[0].extends).toBeDefined(); runCLI(`generate @nx/js:lib ${mylib} --no-interactive`); verifySuccessfulMigratedSetup(myapp, mylib); appEslint = readJson(`.eslintrc.json`); e2eEslint = readJson('e2e/.eslintrc.json'); // should have no plugin extends expect(appEslint.overrides[0].extends).toBeUndefined(); expect(appEslint.overrides[1].extends).toBeUndefined(); expect(e2eEslint.overrides[0].extends).toBeUndefined(); }); }); }); /** * Update the generated rule implementation to produce a known lint error from all files. * * It is important that we do this surgically via AST transformations, otherwise we will * drift further and further away from the original generated code and therefore make our * e2e test less accurate and less valuable. */ function updateGeneratedRuleImplementation( newRulePath: string, newRuleGeneratedContents: string, knownLintErrorMessage: string, messageId, libMethodName: string, libPath: string ): string { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const newRuleSourceFile = ts.createSourceFile( newRulePath, newRuleGeneratedContents, ts.ScriptTarget.Latest, true ); const transformer = ( context: ts.TransformationContext ) => ((rootNode: T) => { function visit(node: ts.Node): ts.Node { /** * Add an ESLint messageId which will show the knownLintErrorMessage * * i.e. * * messages: { * e2eMessageId: knownLintErrorMessage * } */ if ( ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.escapedText === 'messages' ) { return ts.factory.updatePropertyAssignment( node, node.name, ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment( messageId, ts.factory.createStringLiteral(knownLintErrorMessage) ), ]) ); } /** * Update the rule implementation to report the knownLintErrorMessage on every Program node * * During the debugging of the switch from ts-node to swc-node we found out * that regular rules would work even without explicit path mapping registration, * but rules that import runtime functionality from within the workspace * would break the rule registration. * * Instead of having a static literal messageId we retreieved it via imported getMessageId method. * * i.e. * * create(context) { * return { * Program(node) { * context.report({ * messageId: getMessageId(), * node, * }); * } * } * } */ if ( ts.isMethodDeclaration(node) && ts.isIdentifier(node.name) && node.name.escapedText === 'create' ) { return ts.factory.updateMethodDeclaration( node, getModifiers(node), node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, ts.factory.createBlock([ ts.factory.createReturnStatement( ts.factory.createObjectLiteralExpression([ ts.factory.createMethodDeclaration( [], undefined, 'Program', undefined, [], [ ts.factory.createParameterDeclaration( [], undefined, 'node', undefined, undefined, undefined ), ], undefined, ts.factory.createBlock([ ts.factory.createExpressionStatement( ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier('context'), 'report' ), [], [ ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment( 'messageId', ts.factory.createCallExpression( ts.factory.createIdentifier(libMethodName), [], [] ) ), ts.factory.createShorthandPropertyAssignment( 'node' ), ]), ] ) ), ]) ), ]) ), ]) ); } return ts.visitEachChild(node, visit, context); } /** * Add lib import as a first line of the rule file. * Needed for the access of getMessageId in the context report above. * * i.e. * * import { getMessageId } from "@myproj/mylib"; * */ const importAdded = ts.factory.updateSourceFile(rootNode, [ ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause( false, undefined, ts.factory.createNamedImports([ ts.factory.createImportSpecifier( false, undefined, ts.factory.createIdentifier(libMethodName) ), ]) ), ts.factory.createStringLiteral(libPath) ), ...rootNode.statements, ]); return ts.visitNode(importAdded, visit); }) as ts.Transformer; const result: ts.TransformationResult = ts.transform(newRuleSourceFile, [transformer]); const updatedSourceFile: ts.SourceFile = result.transformed[0]; return printer.printFile(updatedSourceFile); }