From 16614702c364b3446b0c03d439d530c4661513c7 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 9 Jun 2020 17:33:25 +0200 Subject: [PATCH] chore: re-write @monocdk-experiment/rewrite-imports (#8401) Improve the reliability of `@monocdk-experiment/rewrite-imports` by making it use the TypeScript compiler to locate import statements that need re-writing, and performing the relevant surgery on the source code based on the findings. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../rewrite-imports/bin/rewrite-imports.ts | 11 +- .../rewrite-imports/lib/rewrite.ts | 125 +++++++++++++++--- .../rewrite-imports/package.json | 3 +- .../rewrite-imports/test/rewrite.test.ts | 86 ++++++++---- 4 files changed, 171 insertions(+), 54 deletions(-) diff --git a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts index 360e526fbe175..a79f833d5e71c 100644 --- a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts +++ b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts @@ -3,10 +3,9 @@ import * as fs from 'fs'; import * as _glob from 'glob'; import { promisify } from 'util'; -import { rewriteFile } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; + const glob = promisify(_glob); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); async function main() { if (!process.argv[2]) { @@ -21,10 +20,10 @@ async function main() { const files = await glob(process.argv[2], { ignore, matchBase: true }); for (const file of files) { - const input = await readFile(file, 'utf-8'); - const output = rewriteFile(input); + const input = await fs.promises.readFile(file, { encoding: 'utf8' }); + const output = rewriteImports(input, file); if (output.trim() !== input.trim()) { - await writeFile(file, output); + await fs.promises.writeFile(file, output); } } } diff --git a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts index 35b78943f6444..f6dad5edbd89b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts +++ b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts @@ -1,24 +1,113 @@ -const exclude = [ - '@aws-cdk/cloudformation-diff', - '@aws-cdk/assert', -]; +import * as ts from 'typescript'; + +/** + * Re-writes "hyper-modular" CDK imports (most packages in `@aws-cdk/*`) to the + * relevant "mono" CDK import path. The re-writing will only modify the imported + * library path, presrving the existing quote style, etc... + * + * Syntax errors in the source file being processed may cause some import + * statements to not be re-written. + * + * Supported import statement forms are: + * - `import * as lib from '@aws-cdk/lib';` + * - `import { Type } from '@aws-cdk/lib';` + * - `import '@aws-cdk/lib';` + * - `import lib = require('@aws-cdk/lib');` + * - `import { Type } = require('@aws-cdk/lib'); + * - `require('@aws-cdk/lib'); + * + * @param sourceText the source code where imports should be re-written. + * @param fileName a customized file name to provide the TypeScript processor. + * + * @returns the updated source code. + */ +export function rewriteImports(sourceText: string, fileName: string = 'index.ts'): string { + const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES2018); + + const replacements = new Array<{ original: ts.Node, updatedLocation: string }>(); + + const visitor = (node: T): ts.VisitResult => { + const moduleSpecifier = getModuleSpecifier(node); + const newTarget = moduleSpecifier && updatedLocationOf(moduleSpecifier.text); + + if (moduleSpecifier != null && newTarget != null) { + replacements.push({ original: moduleSpecifier, updatedLocation: newTarget }); + } -export function rewriteFile(source: string) { - const output = new Array(); - for (const line of source.split('\n')) { - output.push(rewriteLine(line)); + return node; + }; + + sourceFile.statements.forEach(node => ts.visitNode(node, visitor)); + + let updatedSourceText = sourceText; + // Applying replacements in reverse order, so node positions remain valid. + for (const replacement of replacements.sort(({ original: l }, { original: r }) => r.getStart(sourceFile) - l.getStart(sourceFile))) { + const prefix = updatedSourceText.substring(0, replacement.original.getStart(sourceFile) + 1); + const suffix = updatedSourceText.substring(replacement.original.getEnd() - 1); + + updatedSourceText = prefix + replacement.updatedLocation + suffix; } - return output.join('\n'); -} -export function rewriteLine(line: string) { - for (const skip of exclude) { - if (line.includes(skip)) { - return line; + return updatedSourceText; + + function getModuleSpecifier(node: ts.Node): ts.StringLiteral | undefined { + if (ts.isImportDeclaration(node)) { + // import style + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + // import from 'location'; + // import * as name from 'location'; + return moduleSpecifier; + } else if (ts.isBinaryExpression(moduleSpecifier) && ts.isCallExpression(moduleSpecifier.right)) { + // import { Type } = require('location'); + return getModuleSpecifier(moduleSpecifier.right); + } + } else if ( + ts.isImportEqualsDeclaration(node) + && ts.isExternalModuleReference(node.moduleReference) + && ts.isStringLiteral(node.moduleReference.expression) + ) { + // import name = require('location'); + return node.moduleReference.expression; + } else if ( + (ts.isCallExpression(node)) + && ts.isIdentifier(node.expression) + && node.expression.escapedText === 'require' + && node.arguments.length === 1 + ) { + // require('location'); + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + return argument; + } + } else if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) { + // require('location'); // This is an alternate AST version of it + return getModuleSpecifier(node.expression); } + return undefined; } - return line - .replace(/(["'])@aws-cdk\/assert(["'])/g, '$1@monocdk-experiment/assert$2') // @aws-cdk/assert => @monocdk-experiment/assert - .replace(/(["'])@aws-cdk\/core(["'])/g, '$1monocdk-experiment$2') // @aws-cdk/core => monocdk-experiment - .replace(/(["'])@aws-cdk\/(.+)(["'])/g, '$1monocdk-experiment/$2$3'); // @aws-cdk/* => monocdk-experiment/*; +} + +const EXEMPTIONS = new Set([ + '@aws-cdk/cloudformation-diff', +]); + +function updatedLocationOf(modulePath: string): string | undefined { + if (!modulePath.startsWith('@aws-cdk/') || EXEMPTIONS.has(modulePath)) { + return undefined; + } + + if (modulePath === '@aws-cdk/core') { + return 'monocdk-experiment'; + } + + if (modulePath === '@aws-cdk/assert') { + return '@monocdk-experiment/assert'; + } + + if (modulePath === '@aws-cdk/assert/jest') { + return '@monocdk-experiment/assert/jest'; + } + + return `monocdk-experiment/${modulePath.substring(9)}`; } diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index 1c1a44d1758a0..d9a1f74eb18e8 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -31,7 +31,8 @@ }, "license": "Apache-2.0", "dependencies": { - "glob": "^7.1.6" + "glob": "^7.1.6", + "typescript": "~3.8.3" }, "devDependencies": { "@types/glob": "^7.1.1", diff --git a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts index d282338f66c4b..689efb72ef79b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts +++ b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts @@ -1,47 +1,75 @@ -import { rewriteFile, rewriteLine } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; -describe('rewriteLine', () => { - test('quotes', () => { - expect(rewriteLine('import * as s3 from \'@aws-cdk/aws-s3\'')) - .toEqual('import * as s3 from \'monocdk-experiment/aws-s3\''); - }); +describe(rewriteImports, () => { + test('correctly rewrites naked "import"', () => { + const output = rewriteImports(` + // something before + import '@aws-cdk/assert/jest'; + // something after - test('double quotes', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/aws-s3"')) - .toEqual('import * as s3 from "monocdk-experiment/aws-s3"'); - }); + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + import '@monocdk-experiment/assert/jest'; + // something after - test('@aws-cdk/core', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/core"')) - .toEqual('import * as s3 from "monocdk-experiment"'); - expect(rewriteLine('import * as s3 from \'@aws-cdk/core\'')) - .toEqual('import * as s3 from \'monocdk-experiment\''); + console.log('Look! I did something!');`); }); - test('non-jsii modules are ignored', () => { - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/cloudformation-diff\'')) - .toEqual('import * as cfndiff from \'@aws-cdk/cloudformation-diff\''); - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/assert')) - .toEqual('import * as cfndiff from \'@aws-cdk/assert'); + test('correctly rewrites naked "require"', () => { + const output = rewriteImports(` + // something before + require('@aws-cdk/assert/jest'); + // something after + + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + require('@monocdk-experiment/assert/jest'); + // something after + + console.log('Look! I did something!');`); }); -}); -describe('rewriteFile', () => { - const output = rewriteFile(` + test('correctly rewrites "import from"', () => { + const output = rewriteImports(` // something before import * as s3 from '@aws-cdk/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from '@aws-cdk/core'; + import { Construct } from "@aws-cdk/core"; // something after - // hello`); + console.log('Look! I did something!');`, 'subject.ts'); - expect(output).toEqual(` + expect(output).toBe(` // something before import * as s3 from 'monocdk-experiment/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from 'monocdk-experiment'; + import { Construct } from "monocdk-experiment"; + // something after + + console.log('Look! I did something!');`); + }); + + test('correctly rewrites "import = require"', () => { + const output = rewriteImports(` + // something before + import s3 = require('@aws-cdk/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("@aws-cdk/core"); // something after - // hello`); -}); \ No newline at end of file + console.log('Look! I did something!');`, 'subject.ts'); + + expect(output).toBe(` + // something before + import s3 = require('monocdk-experiment/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("monocdk-experiment"); + // something after + + console.log('Look! I did something!');`); + }); +});