1
1
import {
2
+ copyFileSync ,
2
3
existsSync ,
3
4
mkdirSync ,
4
5
mkdtempSync ,
@@ -48,7 +49,7 @@ export function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }: P
48
49
const compatRoot = join ( projectRoot , ...( tsc ?. outDir != null ? [ tsc ?. outDir ] : [ ] ) , TYPES_COMPAT ) ;
49
50
rmSync ( compatRoot , { force : true , recursive : true } ) ;
50
51
51
- const rewrites = new Map < `${number } .${number } `, Map < string , string > > ( ) ;
52
+ const rewrites = new Set < `${number } .${number } `> ( ) ;
52
53
53
54
for ( const breakpoint of DOWNLEVEL_BREAKPOINTS ) {
54
55
if ( TS_VERSION . compare ( breakpoint ) <= 0 ) {
@@ -64,11 +65,14 @@ export function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }: P
64
65
const workdir = mkdtempSync ( join ( tmpdir ( ) , `downlevel-dts-${ breakpoint } -${ basename ( projectRoot ) } -` ) ) ;
65
66
try {
66
67
downlevel ( projectRoot , workdir , breakpoint . version ) ;
67
- for ( const dts of walkDirectory ( workdir ) ) {
68
- const original = readFileSync ( join ( projectRoot , dts ) , 'utf-8' ) ;
69
- const downleveled = readFileSync ( join ( workdir , dts ) , 'utf-8' ) ;
70
- needed ||= ! equalWithNormalizedLineTerminator ( original , downleveled ) ;
71
- rewriteSet . set ( dts , downleveled ) ;
68
+ const projectOutDir = tsc ?. outDir != null ? join ( projectRoot , tsc . outDir ) : projectRoot ;
69
+ const workOutDir = tsc ?. outDir != null ? join ( workdir , tsc . outDir ) : workdir ;
70
+ for ( const dts of walkDirectory ( workOutDir ) ) {
71
+ const original = readFileSync ( join ( projectOutDir , dts ) , 'utf-8' ) ;
72
+ const downleveledPath = join ( workOutDir , dts ) ;
73
+ const downleveled = readFileSync ( downleveledPath , 'utf-8' ) ;
74
+ needed ||= ! semanticallyEqualDeclarations ( original , downleveled ) ;
75
+ rewriteSet . set ( dts , downleveledPath ) ;
72
76
}
73
77
74
78
// If none of the declarations files changed during the down-level, then
@@ -78,7 +82,29 @@ export function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }: P
78
82
// actually does not allow most of the unsupported syntaxes to be used
79
83
// anyway.
80
84
if ( needed ) {
81
- rewrites . set ( `${ breakpoint . major } .${ breakpoint . minor } ` , rewriteSet ) ;
85
+ rewrites . add ( `${ breakpoint . major } .${ breakpoint . minor } ` ) ;
86
+
87
+ const versionSuffix = `ts${ breakpoint . major } .${ breakpoint . minor } ` ;
88
+ const compatDir = join ( compatRoot , versionSuffix ) ;
89
+ if ( ! existsSync ( compatDir ) ) {
90
+ mkdirSync ( compatDir , { recursive : true } ) ;
91
+ try {
92
+ // Write an empty .npmignore file so that npm pack doesn't use the .gitignore file...
93
+ writeFileSync ( join ( compatRoot , '.npmignore' ) , '\n' , 'utf-8' ) ;
94
+ // Make sure all of this is gitignored, out of courtesy...
95
+ writeFileSync ( join ( compatRoot , '.gitignore' ) , '*\n' , 'utf-8' ) ;
96
+ } catch {
97
+ // Ignore any error here... This is inconsequential.
98
+ }
99
+ }
100
+
101
+ for ( const [ dts , downleveledPath ] of rewriteSet ) {
102
+ const rewritten = join ( compatDir , dts ) ;
103
+ // Make sure the parent directory exists (dts might be nested)
104
+ mkdirSync ( dirname ( rewritten ) , { recursive : true } ) ;
105
+ // Write the re-written declarations file there...
106
+ copyFileSync ( downleveledPath , rewritten ) ;
107
+ }
82
108
}
83
109
} finally {
84
110
// Clean up after outselves...
@@ -88,33 +114,11 @@ export function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }: P
88
114
89
115
let typesVersions : Mutable < PackageJson [ 'typesVersions' ] > ;
90
116
91
- for ( const [ version , rewriteSet ] of rewrites ) {
92
- const versionSuffix = `ts${ version } ` ;
93
- const compatDir = join ( compatRoot , versionSuffix ) ;
94
- if ( ! existsSync ( compatDir ) ) {
95
- mkdirSync ( compatDir , { recursive : true } ) ;
96
- try {
97
- // Write an empty .npmignore file so that npm pack doesn't use the .gitignore file...
98
- writeFileSync ( join ( compatRoot , '.npmignore' ) , '\n' , 'utf-8' ) ;
99
- // Make sure all of this is gitignored, out of courtesy...
100
- writeFileSync ( join ( compatRoot , '.gitignore' ) , '*\n' , 'utf-8' ) ;
101
- } catch {
102
- // Ignore any error here... This is inconsequential.
103
- }
104
- }
105
-
106
- for ( const [ dts , downleveled ] of rewriteSet ) {
107
- const rewritten = join ( compatDir , dts ) ;
108
- // Make sure the parent directory exists (dts might be nested)
109
- mkdirSync ( dirname ( rewritten ) , { recursive : true } ) ;
110
- // Write the re-written declarations file there...
111
- writeFileSync ( rewritten , downleveled , 'utf-8' ) ;
112
- }
113
-
117
+ for ( const version of rewrites ) {
114
118
// Register the type redirect in the typesVersions configuration
115
119
typesVersions ??= { } ;
116
120
const from = [ ...( tsc ?. outDir != null ? [ tsc ?. outDir ] : [ ] ) , '*' ] . join ( '/' ) ;
117
- const to = [ ...( tsc ?. outDir != null ? [ tsc ?. outDir ] : [ ] ) , TYPES_COMPAT , versionSuffix , '*' ] . join ( '/' ) ;
121
+ const to = [ ...( tsc ?. outDir != null ? [ tsc ?. outDir ] : [ ] ) , TYPES_COMPAT , `ts ${ version } ` , '*' ] . join ( '/' ) ;
118
122
// We put 2 candidate redirects (first match wins), so that it works for nested imports, too (see: https://github.com/microsoft/TypeScript/issues/43133)
119
123
typesVersions [ `<=${ version } ` ] = { [ from ] : [ to , `${ to } /index.d.ts` ] } ;
120
124
}
@@ -159,23 +163,37 @@ export function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }: P
159
163
}
160
164
161
165
/**
162
- * Compares two strings ignoring new line differences. This is necessary because
163
- * `downlevel-dts` may use a different line-ending convention (at time of
164
- * writing, it always uses CRLF) than what `jsii` itself emits, but that should
165
- * not affect the comparison of declarations files.
166
+ * Compares the contents of two declaration files semantically.
166
167
*
167
168
* @param left the first string.
168
169
* @param right the second string.
169
170
*
170
- * @returns `true` if `left` and `right` contain the same text, modulo line
171
- * terminator tokens.
171
+ * @returns `true` if `left` and `right` contain the same declarations.
172
172
*/
173
- function equalWithNormalizedLineTerminator ( left : string , right : string ) : boolean {
174
- // ECMA262 12.3: https://tc39.es/ecma262/#sec-line-terminators
175
- const JS_LINE_TERMINATOR_REGEX = / ( \n | \r \n ? | \u{2028} | \u{2029} ) / gmu;
173
+ function semanticallyEqualDeclarations ( left : string , right : string ) : boolean {
174
+ // We normalize declarations largely by parsing & re-printing them.
175
+ const normalizeDeclarations = ( code : string ) : string => {
176
+ const sourceFile = ts . createSourceFile ( 'index.d.ts' , code , ts . ScriptTarget . Latest , false , ts . ScriptKind . TS ) ;
177
+ const printer = ts . createPrinter ( {
178
+ newLine : ts . NewLineKind . LineFeed ,
179
+ noEmitHelpers : true ,
180
+ omitTrailingSemicolon : false ,
181
+ removeComments : true ,
182
+ } ) ;
183
+ let normalized = printer . printFile ( sourceFile ) ;
184
+
185
+ // TypeScript may emit duplicated reference declarations... which are absent from Downlevel-DTS' output...
186
+ // https://github.com/microsoft/TypeScript/issues/48143
187
+ const REFERENCES_TYPES_NODE = '/// <reference types="node" />' ;
188
+ if ( normalized . startsWith ( `${ REFERENCES_TYPES_NODE } \n${ REFERENCES_TYPES_NODE } ` ) ) {
189
+ normalized = normalized . slice ( REFERENCES_TYPES_NODE . length + 1 ) ;
190
+ }
191
+
192
+ return normalized ;
193
+ } ;
176
194
177
- left = left . replace ( JS_LINE_TERMINATOR_REGEX , '\n' ) . trim ( ) ;
178
- right = right . replace ( JS_LINE_TERMINATOR_REGEX , '\n' ) . trim ( ) ;
195
+ left = normalizeDeclarations ( left ) ;
196
+ right = normalizeDeclarations ( right ) ;
179
197
180
198
return left === right ;
181
199
}
0 commit comments