Skip to content

Commit

Permalink
chore: re-write @monocdk-experiment/rewrite-imports (#8401)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
RomainMuller authored Jun 9, 2020
1 parent f656ea7 commit 1661470
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand All @@ -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);
}
}
}
Expand Down
125 changes: 107 additions & 18 deletions packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ts.Node>(node: T): ts.VisitResult<T> => {
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<string>();
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)}`;
}
3 changes: 2 additions & 1 deletion packages/@monocdk-experiment/rewrite-imports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
86 changes: 57 additions & 29 deletions packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts
Original file line number Diff line number Diff line change
@@ -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`);
});
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!');`);
});
});

0 comments on commit 1661470

Please sign in to comment.