@@ -23,6 +23,13 @@ export interface RewriteOptions {
23
23
* The unscoped name of the package, e.g. 'aws-kinesisfirehose'.
24
24
*/
25
25
readonly packageUnscopedName ?: string ;
26
+
27
+ /**
28
+ * When true, imports to known types from the 'constructs' library will be rewritten
29
+ * to explicitly import from 'constructs', rather than '@aws-cdk/core'.
30
+ * @default false
31
+ */
32
+ readonly rewriteConstructsImports ?: boolean ;
26
33
}
27
34
28
35
/**
@@ -48,7 +55,12 @@ export interface RewriteOptions {
48
55
* @returns the updated source code.
49
56
*/
50
57
export function rewriteMonoPackageImports ( sourceText : string , libName : string , fileName : string = 'index.ts' , options : RewriteOptions = { } ) : string {
51
- return rewriteImports ( sourceText , ( modPath , importedElements ) => updatedExternalLocation ( modPath , libName , options , importedElements ) , fileName ) ;
58
+ return rewriteImports (
59
+ sourceText ,
60
+ ( modPath , importedElements ) => updatedExternalLocation ( modPath , libName , options , importedElements ) ,
61
+ fileName ,
62
+ options . rewriteConstructsImports ,
63
+ ) ;
52
64
}
53
65
54
66
/**
@@ -76,7 +88,12 @@ export function rewriteMonoPackageImports(sourceText: string, libName: string, f
76
88
export function rewriteReadmeImports ( sourceText : string , libName : string , fileName : string = 'index.ts' , options : RewriteOptions = { } ) : string {
77
89
return sourceText . replace ( / ( ` ` ` (?: t s | t y p e s c r i p t | t e x t ) [ ^ \n ] * \n ) ( .* ?) ( \n \s * ` ` ` ) / gs, ( _m , prefix , body , suffix ) => {
78
90
return prefix +
79
- rewriteImports ( body , ( modPath , importedElements ) => updatedExternalLocation ( modPath , libName , options , importedElements ) , fileName ) +
91
+ rewriteImports (
92
+ body ,
93
+ ( modPath , importedElements ) => updatedExternalLocation ( modPath , libName , options , importedElements ) ,
94
+ fileName ,
95
+ options . rewriteConstructsImports ,
96
+ ) +
80
97
suffix ;
81
98
} ) ;
82
99
}
@@ -107,79 +124,258 @@ export function rewriteImports(
107
124
sourceText : string ,
108
125
updatedLocation : ( modulePath : string , importedElements ?: ts . NodeArray < ts . ImportSpecifier > ) => string | undefined ,
109
126
fileName : string = 'index.ts' ,
127
+ rewriteConstructsImports : boolean = false ,
110
128
) : string {
111
- const sourceFile = ts . createSourceFile ( fileName , sourceText , ts . ScriptTarget . ES2018 ) ;
129
+ const sourceFile = ts . createSourceFile ( fileName , sourceText , ts . ScriptTarget . ES2018 , true ) ;
130
+ const rewriter = new ImportRewriter ( sourceFile , updatedLocation , rewriteConstructsImports ) ;
131
+ ts . transform ( sourceFile , [ rewriter . rewriteTransformer ( ) ] ) ;
132
+ return rewriter . rewriteImports ( ) ;
133
+ }
134
+
135
+ class ImportRewriter {
136
+ private static CONSTRUCTS_TYPES = [ 'Construct' , 'IConstruct' ] ;
137
+
138
+ private readonly replacements = new Array < { original : ts . Node , updatedLocation : string , quoted : boolean } > ( ) ;
139
+ // Constructs rewrites
140
+ private readonly constructsNamedImports : Set < ts . ImportSpecifier > = new Set ( ) ;
141
+ private readonly constructsId = 'constructs' ;
142
+ private firstImportNode ?: ts . Node ;
143
+ private constructsNamespaceImportRequired : boolean = false ;
144
+
145
+ public constructor (
146
+ private readonly sourceFile : ts . SourceFile ,
147
+ private readonly updatedLocation : ( modulePath : string , importedElements ?: ts . NodeArray < ts . ImportSpecifier > ) => string | undefined ,
148
+ private readonly rewriteConstructsImports : boolean ,
149
+ ) { }
112
150
113
- const replacements = new Array < { original : ts . Node , updatedLocation : string } > ( ) ;
151
+ public rewriteTransformer ( ) : ts . TransformerFactory < ts . SourceFile > {
152
+ const coreNamespaceImports : Set < string > = new Set ( ) ;
114
153
115
- const visitor = < T extends ts . Node > ( node : T ) : ts . VisitResult < T > => {
116
- const moduleSpecifier = getModuleSpecifier ( node ) ;
117
- const newTarget = moduleSpecifier && updatedLocation ( moduleSpecifier . text , getImportedElements ( node ) ) ;
154
+ return ( context ) => {
155
+ return ( sourceFile ) => {
156
+ const visitor = < T extends ts . Node > ( node : T ) : ts . VisitResult < T > => {
157
+ const moduleSpecifier = getModuleSpecifier ( node ) ;
158
+ if ( moduleSpecifier ) {
159
+ return this . visitImportNode < T > ( node , coreNamespaceImports , moduleSpecifier ) ;
160
+ }
118
161
119
- if ( moduleSpecifier != null && newTarget != null ) {
120
- replacements . push ( { original : moduleSpecifier , updatedLocation : newTarget } ) ;
162
+ // Rewrite any access or type references with a format `foo.Construct`,
163
+ // where `foo` matches the name of a namespace import for '@aws-cdk/core'
164
+ // Simple identifiers (e.g., readonly foo: Construct) do not need to be written,
165
+ // only qualified identifiers (e.g., cdk.Construct).
166
+ if ( ts . isIdentifier ( node ) && ImportRewriter . CONSTRUCTS_TYPES . includes ( node . text ) ) {
167
+ if ( ts . isPropertyAccessExpression ( node . parent )
168
+ && ts . isIdentifier ( node . parent . expression )
169
+ && coreNamespaceImports . has ( node . parent . expression . text ) ) {
170
+ this . replacements . push ( { original : node . parent , updatedLocation : `${ this . constructsId } .${ node . text } ` , quoted : false } ) ;
171
+ this . constructsNamespaceImportRequired = true ;
172
+ } else if ( ts . isQualifiedName ( node . parent )
173
+ && ts . isIdentifier ( node . parent . left )
174
+ && coreNamespaceImports . has ( node . parent . left . text ) ) {
175
+ this . replacements . push ( { original : node . parent , updatedLocation : `${ this . constructsId } .${ node . text } ` , quoted : false } ) ;
176
+ this . constructsNamespaceImportRequired = true ;
177
+ }
178
+ }
179
+
180
+ return ts . visitEachChild ( node , visitor , context ) ;
181
+ } ;
182
+
183
+ return ts . visitNode ( sourceFile , visitor ) ;
184
+ } ;
185
+ } ;
186
+ }
187
+
188
+ /**
189
+ * Visit import nodes where a module specifier of some kind has been found.
190
+ *
191
+ * For most nodes, this simply involves rewritting the location of the module via `this.updatedLocation`.
192
+ *
193
+ * Assumes the current node is an import (of some type) that imports '@aws-cdk/core'.
194
+ *
195
+ * The following import types are suported:
196
+ * - import * as core1 from '@aws-cdk/core';
197
+ * - import core2 = require('@aws-cdk/core');
198
+ * - import { Type1, Type2 as CoreType2 } from '@aws-cdk/core';
199
+ * - import { Type1, Type2 as CoreType2 } = require('@aws-cdk/core');
200
+ *
201
+ * For all namespace imports, capture the namespace used so any references later can be updated.
202
+ * For example, 'core1.Construct' needs to be renamed to 'constructs.Construct'.
203
+ * For all named imports:
204
+ * - If all named imports are constructs types, simply rename the import from core to constructs.
205
+ * - If there's a split, the constructs types are removed and captured for later to go into a new import.
206
+ *
207
+ * @returns true iff all other transforms should be skipped for this node.
208
+ */
209
+ private visitImportNode < T extends ts . Node > ( node : T , coreNamespaceImports : Set < string > , moduleSpecifier : ts . StringLiteral ) {
210
+ // Used later for constructs imports generation, to mark location and get indentation
211
+ if ( ! this . firstImportNode ) { this . firstImportNode = node ; }
212
+
213
+ // Special-case @aws-cdk/core for the case of constructs imports.
214
+ if ( this . rewriteConstructsImports && moduleSpecifier . text === '@aws-cdk/core' ) {
215
+ if ( ts . isImportEqualsDeclaration ( node ) ) {
216
+ // import core = require('@aws-cdk/core');
217
+ coreNamespaceImports . add ( node . name . text ) ;
218
+ } else if ( ts . isImportDeclaration ( node ) && node . importClause ?. namedBindings ) {
219
+ const bindings = node . importClause ?. namedBindings ;
220
+ if ( ts . isNamespaceImport ( bindings ) ) {
221
+ // import * as core from '@aws-cdk/core';
222
+ coreNamespaceImports . add ( bindings . name . text ) ;
223
+ } else if ( ts . isNamedImports ( bindings ) ) {
224
+ // import { Type1, Type2 as CoreType2 } from '@aws-cdk/core';
225
+ // import { Type1, Type2 as CoreType2 } = require('@aws-cdk/core');
226
+
227
+ // Segment the types into core vs construct types
228
+ const constructsImports : ts . ImportSpecifier [ ] = [ ] ;
229
+ const coreImports : ts . ImportSpecifier [ ] = [ ] ;
230
+ bindings . elements . forEach ( ( e ) => {
231
+ if ( ImportRewriter . CONSTRUCTS_TYPES . includes ( e . name . text ) ||
232
+ ( e . propertyName && ImportRewriter . CONSTRUCTS_TYPES . includes ( e . propertyName . text ) ) ) {
233
+ constructsImports . push ( e ) ;
234
+ } else {
235
+ coreImports . push ( e ) ;
236
+ }
237
+ } ) ;
238
+
239
+ // Three cases:
240
+ // 1. There are no constructs imports. No special-casing to do.
241
+ // 2. There are ONLY constructs imports. The whole import can be replaced.
242
+ // 3. There is a mix. We must remove the constructs imports, and add them to a dedicated line.
243
+ if ( constructsImports . length > 0 ) {
244
+ if ( coreImports . length === 0 ) {
245
+ // Rewrite the module to constructs, skipping the normal updateLocation replacement.
246
+ this . replacements . push ( { original : moduleSpecifier , updatedLocation : this . constructsId , quoted : true } ) ;
247
+ return node ;
248
+ } else {
249
+ // Track these named imports to add to a dedicated import statement later.
250
+ constructsImports . forEach ( ( i ) => this . constructsNamedImports . add ( i ) ) ;
251
+
252
+ // This replaces the interior of the import statement, between the braces:
253
+ // import { Stack as CdkStack, StackProps } ...
254
+ const coreBindings = ' ' + coreImports . map ( ( e ) => e . getText ( ) ) . join ( ', ' ) + ' ' ;
255
+ this . replacements . push ( { original : bindings , updatedLocation : coreBindings , quoted : true } ) ;
256
+ }
257
+ }
258
+ }
259
+ }
121
260
}
122
261
262
+ const newTarget = this . updatedLocation ( moduleSpecifier . text , getImportedElements ( node ) ) ;
263
+ if ( newTarget != null ) {
264
+ this . replacements . push ( { original : moduleSpecifier , updatedLocation : newTarget , quoted : true } ) ;
265
+ }
123
266
return node ;
124
- } ;
267
+ }
125
268
126
- sourceFile . statements . forEach ( node => ts . visitNode ( node , visitor ) ) ;
269
+ /**
270
+ * Rewrites the imports -- and possibly some qualified identifiers -- in the source file,
271
+ * based on the replacement information gathered via transforming the source through `rewriteTransformer()`.
272
+ */
273
+ public rewriteImports ( ) : string {
274
+ let updatedSourceText = this . sourceFile . text ;
275
+ // Applying replacements in reverse order, so node positions remain valid.
276
+ const sortedReplacements = this . replacements . sort (
277
+ ( { original : l } , { original : r } ) => r . getStart ( this . sourceFile ) - l . getStart ( this . sourceFile ) ) ;
278
+ for ( const replacement of sortedReplacements ) {
279
+ const offset = replacement . quoted ? 1 : 0 ;
280
+ const prefix = updatedSourceText . substring ( 0 , replacement . original . getStart ( this . sourceFile ) + offset ) ;
281
+ const suffix = updatedSourceText . substring ( replacement . original . getEnd ( ) - offset ) ;
282
+
283
+ updatedSourceText = prefix + replacement . updatedLocation + suffix ;
284
+ }
127
285
128
- let updatedSourceText = sourceText ;
129
- // Applying replacements in reverse order, so node positions remain valid.
130
- for ( const replacement of replacements . sort ( ( { original : l } , { original : r } ) => r . getStart ( sourceFile ) - l . getStart ( sourceFile ) ) ) {
131
- const prefix = updatedSourceText . substring ( 0 , replacement . original . getStart ( sourceFile ) + 1 ) ;
132
- const suffix = updatedSourceText . substring ( replacement . original . getEnd ( ) - 1 ) ;
286
+ // Lastly, prepend the source with any new constructs imports, as needed.
287
+ const constructsImports = this . getConstructsImportsPrefix ( ) ;
288
+ if ( constructsImports ) {
289
+ const insertionPoint = this . firstImportNode
290
+ // Start of the line, past any leading comments or shebang lines
291
+ ? ( this . firstImportNode . getStart ( ) - this . getNodeIndentation ( this . firstImportNode ) )
292
+ : 0 ;
293
+ updatedSourceText = updatedSourceText . substring ( 0 , insertionPoint )
294
+ + constructsImports
295
+ + updatedSourceText . substring ( insertionPoint ) ;
296
+ }
133
297
134
- updatedSourceText = prefix + replacement . updatedLocation + suffix ;
298
+ return updatedSourceText ;
135
299
}
136
300
137
- return updatedSourceText ;
301
+ /**
302
+ * If constructs imports are needed (either namespaced or named types),
303
+ * this returns a string with one (or both) imports that can be prepended to the source.
304
+ */
305
+ private getConstructsImportsPrefix ( ) : string | undefined {
306
+ if ( ! this . constructsNamespaceImportRequired && this . constructsNamedImports . size === 0 ) { return undefined ; }
307
+
308
+ const indentation = ' ' . repeat ( this . getNodeIndentation ( this . firstImportNode ) ) ;
309
+ let constructsImportPrefix = '' ;
310
+ if ( this . constructsNamespaceImportRequired ) {
311
+ constructsImportPrefix += `${ indentation } import * as ${ this . constructsId } from 'constructs';\n` ;
312
+ }
313
+ if ( this . constructsNamedImports . size > 0 ) {
314
+ const namedImports = [ ...this . constructsNamedImports ] . map ( i => i . getText ( ) ) . join ( ', ' ) ;
315
+ constructsImportPrefix += `${ indentation } import { ${ namedImports } } from 'constructs';\n` ;
316
+ }
317
+ return constructsImportPrefix ;
318
+ }
138
319
139
320
/**
140
- * Returns the module specifier (location) of an import statement in one of the following forms:
141
- * import from 'location';
142
- * import * as name from 'location';
143
- * import { Type } = require('location');
144
- * import name = require('location');
145
- * require('location');
321
+ * For a given node, attempts to determine and return how many spaces of indentation are used.
146
322
*/
147
- function getModuleSpecifier ( node : ts . Node ) : ts . StringLiteral | undefined {
148
- if ( ts . isImportDeclaration ( node ) ) {
149
- // import style
150
- const moduleSpecifier = node . moduleSpecifier ;
151
- if ( ts . isStringLiteral ( moduleSpecifier ) ) {
152
- // import from 'location';
153
- // import * as name from 'location';
154
- return moduleSpecifier ;
155
- } else if ( ts . isBinaryExpression ( moduleSpecifier ) && ts . isCallExpression ( moduleSpecifier . right ) ) {
156
- // import { Type } = require('location');
157
- return getModuleSpecifier ( moduleSpecifier . right ) ;
158
- }
159
- } else if (
160
- ts . isImportEqualsDeclaration ( node )
323
+ private getNodeIndentation ( node ?: ts . Node ) : number {
324
+ if ( ! node ) { return 0 ; }
325
+
326
+ // Get leading spaces for the final line in the node's trivia
327
+ const fullText = node . getFullText ( ) ;
328
+ const trivia = fullText . substring ( 0 , fullText . length - node . getWidth ( ) ) ;
329
+ const m = / ( * ) $ / . exec ( trivia ) ;
330
+ return m ? m [ 1 ] . length : 0 ;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Returns the module specifier (location) of an import statement in one of the following forms:
336
+ * import from 'location';
337
+ * import * as name from 'location';
338
+ * import { Type } from 'location';
339
+ * import { Type } = require('location');
340
+ * import name = require('location');
341
+ * require('location');
342
+ */
343
+ function getModuleSpecifier ( node : ts . Node ) : ts . StringLiteral | undefined {
344
+ if ( ts . isImportDeclaration ( node ) ) {
345
+ // import style
346
+ const moduleSpecifier = node . moduleSpecifier ;
347
+ if ( ts . isStringLiteral ( moduleSpecifier ) ) {
348
+ // import from 'location';
349
+ // import * as name from 'location';
350
+ // import { Foo } from 'location';
351
+ return moduleSpecifier ;
352
+ } else if ( ts . isBinaryExpression ( moduleSpecifier ) && ts . isCallExpression ( moduleSpecifier . right ) ) {
353
+ // import { Type } = require('location');
354
+ return getModuleSpecifier ( moduleSpecifier . right ) ;
355
+ }
356
+ } else if (
357
+ ts . isImportEqualsDeclaration ( node )
161
358
&& ts . isExternalModuleReference ( node . moduleReference )
162
359
&& ts . isStringLiteral ( node . moduleReference . expression )
163
- ) {
164
- // import name = require('location');
165
- return node . moduleReference . expression ;
166
- } else if (
167
- ( ts . isCallExpression ( node ) )
360
+ ) {
361
+ // import name = require('location');
362
+ return node . moduleReference . expression ;
363
+ } else if (
364
+ ( ts . isCallExpression ( node ) )
168
365
&& ts . isIdentifier ( node . expression )
169
366
&& node . expression . escapedText === 'require'
170
367
&& node . arguments . length === 1
171
- ) {
172
- // require('location');
173
- const argument = node . arguments [ 0 ] ;
174
- if ( ts . isStringLiteral ( argument ) ) {
175
- return argument ;
176
- }
177
- } else if ( ts . isExpressionStatement ( node ) && ts . isCallExpression ( node . expression ) ) {
178
- // require('location'); // This is an alternate AST version of it
179
- return getModuleSpecifier ( node . expression ) ;
368
+ ) {
369
+ // require('location');
370
+ const argument = node . arguments [ 0 ] ;
371
+ if ( ts . isStringLiteral ( argument ) ) {
372
+ return argument ;
180
373
}
181
- return undefined ;
374
+ } else if ( ts . isExpressionStatement ( node ) && ts . isCallExpression ( node . expression ) ) {
375
+ // require('location'); // This is an alternate AST version of it
376
+ return getModuleSpecifier ( node . expression ) ;
182
377
}
378
+ return undefined ;
183
379
}
184
380
185
381
const EXEMPTIONS = new Set ( [
0 commit comments