Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: re-write @monocdk-experiment/rewrite-imports #8401

Merged
merged 2 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!');`);
});
});