UNPKG

10.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.emitDownleveledDeclarations = exports.TYPES_COMPAT = void 0;
4const node_fs_1 = require("node:fs");
5const node_os_1 = require("node:os");
6const node_path_1 = require("node:path");
7const downlevel_dts_1 = require("downlevel-dts");
8const log4js = require("log4js");
9const semver_1 = require("semver");
10const ts = require("typescript");
11exports.TYPES_COMPAT = '.types-compat';
12const LOG = log4js.getLogger('jsii/compiler');
13const TS_VERSION = new semver_1.SemVer(`${ts.versionMajorMinor}.0`);
14/**
15 * Declares what versions of the TypeScript language will be supported by the
16 * declarations files (and `typesVersions` entries) produced by this compiler
17 * release.
18 *
19 * This should contain only `major.minor` specifiers, similar to the value of
20 * the `ts.versionMajorMinor` property, and must be sorted in ascending version
21 * order, as this dictates the order of entries in the `typesVersions` redirects
22 * which has a direct impact on resolution (first match wins), and we don't want
23 * to have to perform a sort pass on this list.
24 */
25const DOWNLEVEL_BREAKPOINTS = ['3.9'].map((ver) => new semver_1.SemVer(`${ver}.0`));
26/**
27 * Produces down-leveled declaration files to ensure compatibility with previous
28 * compiler releases (macthing TypeScript's `major.minor` versioning scheme).
29 * This is necessary in order to ensure a package change compiler release lines
30 * does not force all it's consumers to do the same (and vice-versa).
31 *
32 * @returns the `typesVersions` object that should be recorded in `package.json`
33 */
34function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }) {
35 const compatRoot = (0, node_path_1.join)(projectRoot, ...(tsc?.outDir != null ? [tsc?.outDir] : []), exports.TYPES_COMPAT);
36 (0, node_fs_1.rmSync)(compatRoot, { force: true, recursive: true });
37 const rewrites = new Set();
38 for (const breakpoint of DOWNLEVEL_BREAKPOINTS) {
39 if (TS_VERSION.compare(breakpoint) <= 0) {
40 // This TypeScript release is older or same as the breakpoint, so no need
41 // for down-leveling here.
42 continue;
43 }
44 const rewriteSet = new Map();
45 let needed = false;
46 // We'll emit down-leveled declarations in a temporary directory...
47 const workdir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), `downlevel-dts-${breakpoint}-${(0, node_path_1.basename)(projectRoot)}-`));
48 try {
49 (0, downlevel_dts_1.main)(projectRoot, workdir, breakpoint.version);
50 const projectOutDir = tsc?.outDir != null ? (0, node_path_1.join)(projectRoot, tsc.outDir) : projectRoot;
51 const workOutDir = tsc?.outDir != null ? (0, node_path_1.join)(workdir, tsc.outDir) : workdir;
52 for (const dts of walkDirectory(workOutDir)) {
53 const original = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectOutDir, dts), 'utf-8');
54 const downleveledPath = (0, node_path_1.join)(workOutDir, dts);
55 const downleveled = (0, node_fs_1.readFileSync)(downleveledPath, 'utf-8');
56 needed || (needed = !semanticallyEqualDeclarations(original, downleveled));
57 rewriteSet.set(dts, downleveledPath);
58 }
59 // If none of the declarations files changed during the down-level, then
60 // we don't need to actually write it out & cause a redirect. This would
61 // be wasteful. Most codebases won't incur any rewrite at all, since the
62 // declarations files only reference "visible" members, and `jsii`
63 // actually does not allow most of the unsupported syntaxes to be used
64 // anyway.
65 if (needed) {
66 rewrites.add(`${breakpoint.major}.${breakpoint.minor}`);
67 const versionSuffix = `ts${breakpoint.major}.${breakpoint.minor}`;
68 const compatDir = (0, node_path_1.join)(compatRoot, versionSuffix);
69 if (!(0, node_fs_1.existsSync)(compatDir)) {
70 (0, node_fs_1.mkdirSync)(compatDir, { recursive: true });
71 try {
72 // Write an empty .npmignore file so that npm pack doesn't use the .gitignore file...
73 (0, node_fs_1.writeFileSync)((0, node_path_1.join)(compatRoot, '.npmignore'), '\n', 'utf-8');
74 // Make sure all of this is gitignored, out of courtesy...
75 (0, node_fs_1.writeFileSync)((0, node_path_1.join)(compatRoot, '.gitignore'), '*\n', 'utf-8');
76 }
77 catch {
78 // Ignore any error here... This is inconsequential.
79 }
80 }
81 for (const [dts, downleveledPath] of rewriteSet) {
82 const rewritten = (0, node_path_1.join)(compatDir, dts);
83 // Make sure the parent directory exists (dts might be nested)
84 (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(rewritten), { recursive: true });
85 // Write the re-written declarations file there...
86 (0, node_fs_1.copyFileSync)(downleveledPath, rewritten);
87 }
88 }
89 }
90 finally {
91 // Clean up after outselves...
92 (0, node_fs_1.rmSync)(workdir, { force: true, recursive: true });
93 }
94 }
95 let typesVersions;
96 for (const version of rewrites) {
97 // Register the type redirect in the typesVersions configuration
98 typesVersions ?? (typesVersions = {});
99 const from = [...(tsc?.outDir != null ? [tsc?.outDir] : []), '*'].join('/');
100 const to = [...(tsc?.outDir != null ? [tsc?.outDir] : []), exports.TYPES_COMPAT, `ts${version}`, '*'].join('/');
101 // We put 2 candidate redirects (first match wins), so that it works for nested imports, too (see: https://github.com/microsoft/TypeScript/issues/43133)
102 typesVersions[`<=${version}`] = { [from]: [to, `${to}/index.d.ts`] };
103 }
104 // Compare JSON stringifications, as the order of keys is important here...
105 if (JSON.stringify(packageJson.typesVersions) === JSON.stringify(typesVersions)) {
106 // The existing configuration matches the new one. We're done here.
107 return;
108 }
109 LOG.info('The required `typesVersions` configuration has changed. Updating "package.json" accordingly...');
110 // Prepare the new contents of `PackageJson`.
111 const newPackageJson = Object.entries(packageJson).reduce((obj, [key, value]) => {
112 // NB: "as any" below are required becuase we must ignore `readonly` attributes from the source.
113 if (key === 'typesVersions') {
114 if (typesVersions != null) {
115 obj[key] = typesVersions;
116 }
117 }
118 else {
119 obj[key] = value;
120 // If there isn't currently a `typesVersions` entry, but there is a `types` entry,
121 // we'll insert `typesVersions` right after `types`.
122 if (key === 'types' && typesVersions != null && !('typesVersions' in packageJson)) {
123 obj.typesVersions = typesVersions;
124 }
125 }
126 return obj;
127 }, {});
128 // If there was neither `types` nor `typesVersions` in the original `package.json`, we'll
129 // add `typesVersions` at the end of it.
130 if (!('typesVersions' in newPackageJson)) {
131 newPackageJson.typesVersions = typesVersions;
132 }
133 const packageJsonFile = (0, node_path_1.join)(projectRoot, 'package.json');
134 // We try "hard" to preserve the existing indent in the `package.json` file when updating it.
135 const [, indent] = (0, node_fs_1.readFileSync)(packageJsonFile, 'utf-8').match(/^(\s*)"/m) ?? [null, 2];
136 (0, node_fs_1.writeFileSync)(packageJsonFile, `${JSON.stringify(newPackageJson, undefined, indent)}\n`, 'utf-8');
137}
138exports.emitDownleveledDeclarations = emitDownleveledDeclarations;
139/**
140 * Compares the contents of two declaration files semantically.
141 *
142 * @param left the first string.
143 * @param right the second string.
144 *
145 * @returns `true` if `left` and `right` contain the same declarations.
146 */
147function semanticallyEqualDeclarations(left, right) {
148 // We normalize declarations largely by parsing & re-printing them.
149 const normalizeDeclarations = (code) => {
150 const sourceFile = ts.createSourceFile('index.d.ts', code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
151 const printer = ts.createPrinter({
152 newLine: ts.NewLineKind.LineFeed,
153 noEmitHelpers: true,
154 omitTrailingSemicolon: false,
155 removeComments: true,
156 });
157 let normalized = printer.printFile(sourceFile);
158 // TypeScript may emit duplicated reference declarations... which are absent from Downlevel-DTS' output...
159 // https://github.com/microsoft/TypeScript/issues/48143
160 const REFERENCES_TYPES_NODE = '/// <reference types="node" />';
161 while (normalized.startsWith(`${REFERENCES_TYPES_NODE}\n${REFERENCES_TYPES_NODE}`)) {
162 normalized = normalized.slice(REFERENCES_TYPES_NODE.length + 1);
163 }
164 return normalized;
165 };
166 left = normalizeDeclarations(left);
167 right = normalizeDeclarations(right);
168 return left === right;
169}
170/**
171 * Recursively traverse the provided directory and yield the relative (to the
172 * specified `root`) paths of all the `.d.ts` files found there.
173 *
174 * @param dir the directory to be walked.
175 * @param root the root to which paths should be relative.
176 */
177function* walkDirectory(dir, root = dir) {
178 for (const file of (0, node_fs_1.readdirSync)(dir)) {
179 const filePath = (0, node_path_1.join)(dir, file);
180 if ((0, node_fs_1.statSync)(filePath).isDirectory()) {
181 // This is a directory, recurse down...
182 yield* walkDirectory(filePath, root);
183 }
184 else if (file.toLowerCase().endsWith('.d.ts')) {
185 // This is a declaration file, yield it...
186 yield (0, node_path_1.relative)(root, filePath);
187 }
188 }
189}
190//# sourceMappingURL=downlevel-dts.js.map
\No newline at end of file