jsii
Version:
[](https://cdk.dev) [;
exports.emitDownleveledDeclarations = exports.TYPES_COMPAT = void 0;
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const downlevel_dts_1 = require("downlevel-dts");
const log4js = require("log4js");
const semver_1 = require("semver");
const ts = require("typescript");
exports.TYPES_COMPAT = '.types-compat';
const LOG = log4js.getLogger('jsii/compiler');
const TS_VERSION = new semver_1.SemVer(`${ts.versionMajorMinor}.0`);
/**
* Declares what versions of the TypeScript language will be supported by the
* declarations files (and `typesVersions` entries) produced by this compiler
* release.
*
* This should contain only `major.minor` specifiers, similar to the value of
* the `ts.versionMajorMinor` property, and must be sorted in ascending version
* order, as this dictates the order of entries in the `typesVersions` redirects
* which has a direct impact on resolution (first match wins), and we don't want
* to have to perform a sort pass on this list.
*/
const DOWNLEVEL_BREAKPOINTS = ['3.9'].map((ver) => new semver_1.SemVer(`${ver}.0`));
/**
* Produces down-leveled declaration files to ensure compatibility with previous
* compiler releases (macthing TypeScript's `major.minor` versioning scheme).
* This is necessary in order to ensure a package change compiler release lines
* does not force all it's consumers to do the same (and vice-versa).
*
* @returns the `typesVersions` object that should be recorded in `package.json`
*/
function emitDownleveledDeclarations({ packageJson, projectRoot, tsc }) {
const compatRoot = (0, node_path_1.join)(projectRoot, ...(tsc?.outDir != null ? [tsc?.outDir] : []), exports.TYPES_COMPAT);
(0, node_fs_1.rmSync)(compatRoot, { force: true, recursive: true });
const rewrites = new Set();
for (const breakpoint of DOWNLEVEL_BREAKPOINTS) {
if (TS_VERSION.compare(breakpoint) <= 0) {
// This TypeScript release is older or same as the breakpoint, so no need
// for down-leveling here.
continue;
}
const rewriteSet = new Map();
let needed = false;
// We'll emit down-leveled declarations in a temporary directory...
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)}-`));
try {
(0, downlevel_dts_1.main)(projectRoot, workdir, breakpoint.version);
const projectOutDir = tsc?.outDir != null ? (0, node_path_1.join)(projectRoot, tsc.outDir) : projectRoot;
const workOutDir = tsc?.outDir != null ? (0, node_path_1.join)(workdir, tsc.outDir) : workdir;
for (const dts of walkDirectory(workOutDir)) {
const original = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectOutDir, dts), 'utf-8');
const downleveledPath = (0, node_path_1.join)(workOutDir, dts);
const downleveled = (0, node_fs_1.readFileSync)(downleveledPath, 'utf-8');
needed || (needed = !semanticallyEqualDeclarations(original, downleveled));
rewriteSet.set(dts, downleveledPath);
}
// If none of the declarations files changed during the down-level, then
// we don't need to actually write it out & cause a redirect. This would
// be wasteful. Most codebases won't incur any rewrite at all, since the
// declarations files only reference "visible" members, and `jsii`
// actually does not allow most of the unsupported syntaxes to be used
// anyway.
if (needed) {
rewrites.add(`${breakpoint.major}.${breakpoint.minor}`);
const versionSuffix = `ts${breakpoint.major}.${breakpoint.minor}`;
const compatDir = (0, node_path_1.join)(compatRoot, versionSuffix);
if (!(0, node_fs_1.existsSync)(compatDir)) {
(0, node_fs_1.mkdirSync)(compatDir, { recursive: true });
try {
// Write an empty .npmignore file so that npm pack doesn't use the .gitignore file...
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(compatRoot, '.npmignore'), '\n', 'utf-8');
// Make sure all of this is gitignored, out of courtesy...
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(compatRoot, '.gitignore'), '*\n', 'utf-8');
}
catch {
// Ignore any error here... This is inconsequential.
}
}
for (const [dts, downleveledPath] of rewriteSet) {
const rewritten = (0, node_path_1.join)(compatDir, dts);
// Make sure the parent directory exists (dts might be nested)
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(rewritten), { recursive: true });
// Write the re-written declarations file there...
(0, node_fs_1.copyFileSync)(downleveledPath, rewritten);
}
}
}
finally {
// Clean up after outselves...
(0, node_fs_1.rmSync)(workdir, { force: true, recursive: true });
}
}
let typesVersions;
for (const version of rewrites) {
// Register the type redirect in the typesVersions configuration
typesVersions ?? (typesVersions = {});
const from = [...(tsc?.outDir != null ? [tsc?.outDir] : []), '*'].join('/');
const to = [...(tsc?.outDir != null ? [tsc?.outDir] : []), exports.TYPES_COMPAT, `ts${version}`, '*'].join('/');
// We put 2 candidate redirects (first match wins), so that it works for nested imports, too (see: https://github.com/microsoft/TypeScript/issues/43133)
typesVersions[`<=${version}`] = { [from]: [to, `${to}/index.d.ts`] };
}
// Compare JSON stringifications, as the order of keys is important here...
if (JSON.stringify(packageJson.typesVersions) === JSON.stringify(typesVersions)) {
// The existing configuration matches the new one. We're done here.
return;
}
LOG.info('The required `typesVersions` configuration has changed. Updating "package.json" accordingly...');
// Prepare the new contents of `PackageJson`.
const newPackageJson = Object.entries(packageJson).reduce((obj, [key, value]) => {
// NB: "as any" below are required becuase we must ignore `readonly` attributes from the source.
if (key === 'typesVersions') {
if (typesVersions != null) {
obj[key] = typesVersions;
}
}
else {
obj[key] = value;
// If there isn't currently a `typesVersions` entry, but there is a `types` entry,
// we'll insert `typesVersions` right after `types`.
if (key === 'types' && typesVersions != null && !('typesVersions' in packageJson)) {
obj.typesVersions = typesVersions;
}
}
return obj;
}, {});
// If there was neither `types` nor `typesVersions` in the original `package.json`, we'll
// add `typesVersions` at the end of it.
if (!('typesVersions' in newPackageJson)) {
newPackageJson.typesVersions = typesVersions;
}
const packageJsonFile = (0, node_path_1.join)(projectRoot, 'package.json');
// We try "hard" to preserve the existing indent in the `package.json` file when updating it.
const [, indent] = (0, node_fs_1.readFileSync)(packageJsonFile, 'utf-8').match(/^(\s*)"/m) ?? [null, 2];
(0, node_fs_1.writeFileSync)(packageJsonFile, `${JSON.stringify(newPackageJson, undefined, indent)}\n`, 'utf-8');
}
exports.emitDownleveledDeclarations = emitDownleveledDeclarations;
/**
* Compares the contents of two declaration files semantically.
*
* @param left the first string.
* @param right the second string.
*
* @returns `true` if `left` and `right` contain the same declarations.
*/
function semanticallyEqualDeclarations(left, right) {
// We normalize declarations largely by parsing & re-printing them.
const normalizeDeclarations = (code) => {
const sourceFile = ts.createSourceFile('index.d.ts', code, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
noEmitHelpers: true,
omitTrailingSemicolon: false,
removeComments: true,
});
let normalized = printer.printFile(sourceFile);
// TypeScript may emit duplicated reference declarations... which are absent from Downlevel-DTS' output...
// https://github.com/microsoft/TypeScript/issues/48143
const REFERENCES_TYPES_NODE = '/// <reference types="node" />';
while (normalized.startsWith(`${REFERENCES_TYPES_NODE}\n${REFERENCES_TYPES_NODE}`)) {
normalized = normalized.slice(REFERENCES_TYPES_NODE.length + 1);
}
return normalized;
};
left = normalizeDeclarations(left);
right = normalizeDeclarations(right);
return left === right;
}
/**
* Recursively traverse the provided directory and yield the relative (to the
* specified `root`) paths of all the `.d.ts` files found there.
*
* @param dir the directory to be walked.
* @param root the root to which paths should be relative.
*/
function* walkDirectory(dir, root = dir) {
for (const file of (0, node_fs_1.readdirSync)(dir)) {
const filePath = (0, node_path_1.join)(dir, file);
if ((0, node_fs_1.statSync)(filePath).isDirectory()) {
// This is a directory, recurse down...
yield* walkDirectory(filePath, root);
}
else if (file.toLowerCase().endsWith('.d.ts')) {
// This is a declaration file, yield it...
yield (0, node_path_1.relative)(root, filePath);
}
}
}
//# sourceMappingURL=downlevel-dts.js.map
;