diff --git a/.projen/deps.json b/.projen/deps.json index 5ed6debb..b0159208 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -156,10 +156,6 @@ "version": "^4", "type": "runtime" }, - { - "name": "downlevel-dts", - "type": "runtime" - }, { "name": "fast-deep-equal", "type": "runtime" diff --git a/.projenrc.ts b/.projenrc.ts index 6925a36f..93792e00 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -216,7 +216,6 @@ project.addDeps( '@jsii/spec', 'case', 'chalk@^4', - 'downlevel-dts', 'fast-deep-equal', 'log4js', 'semver', diff --git a/package.json b/package.json index d19af111..7a519474 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "@jsii/spec": "^1.104.0", "case": "^1.6.3", "chalk": "^4", - "downlevel-dts": "^0.11.0", "fast-deep-equal": "^3.1.3", "log4js": "^6.9.1", "semver": "^7.6.3", diff --git a/src/ambient/downlevel-dts.d.ts b/src/ambient/downlevel-dts.d.ts deleted file mode 100644 index db3c9eed..00000000 --- a/src/ambient/downlevel-dts.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** Hand-written declaration for the downlevel-dts module */ -declare module 'downlevel-dts' { - /** - * Rewrite .d.ts files created by any version of TypeScript so that they work - * with TypeScript 3.4 or later. It does this by converting code with new - * features into code that uses equivalent old features. - * - * @param src the directory containing the original .d.ts files - * @param target the directory in which to place re-written files - * @param targetVersion the target TypeScript version compatibility - * - * @note The "real" signature would allow semver.SemVer instances to be - * provided as `targetVersion`, but some code-path involves an - * `instanceof` test which will fail if somehow `downlevel-dts` is - * provided with its own install of the `semver` dependency, which we - * cannot control, so we disallow using `semver.SemVer` instances here. - */ - export function main(src: string, target: string, targetVersion: string): void; -} diff --git a/src/compiler.ts b/src/compiler.ts index 9e592ecc..357c9079 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -6,9 +6,7 @@ import * as ts from 'typescript'; import { Assembler } from './assembler'; import { findDependencyDirectory } from './common/find-utils'; -import { emitDownleveledDeclarations, TYPES_COMPAT } from './downlevel-dts'; import { Emitter } from './emitter'; -import { normalizeConfigPath } from './helpers'; import { JsiiDiagnostic } from './jsii-diagnostic'; import { ProjectInfo } from './project-info'; import { WARNINGSCODE_FILE_NAME } from './transforms/deprecation-warnings'; @@ -21,6 +19,7 @@ import * as utils from './utils'; const LOG = log4js.getLogger('jsii/compiler'); export const DIAGNOSTICS = 'diagnostics'; export const JSII_DIAGNOSTICS_CODE = 9999; +export const TYPES_COMPAT = '.types-compat'; export interface CompilerOptions { /** The information about the project to be built */ @@ -314,15 +313,6 @@ export class Compiler implements Emitter { LOG.error('Compilation errors prevented the JSII assembly from being created'); } - if (!hasErrors) { - emitDownleveledDeclarations( - this.projectRoot, - this.options.projectInfo.packageJson, - // outDir might be absolute. Need to normalize it. - normalizeConfigPath(this.projectRoot, this.tsconfig.compilerOptions.outDir), - ); - } - // Some extra validation on the config. // Make sure that { "./.warnings.jsii.js": "./.warnings.jsii.js" } is in the set of // exports, if they are specified. diff --git a/src/downlevel-dts.ts b/src/downlevel-dts.ts deleted file mode 100644 index ef020b00..00000000 --- a/src/downlevel-dts.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { - copyFileSync, - existsSync, - mkdirSync, - mkdtempSync, - readdirSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; -import { basename, dirname, join, relative } from 'node:path'; -import { main as downlevel } from 'downlevel-dts'; -import * as log4js from 'log4js'; -import { SemVer } from 'semver'; -import * as ts from 'typescript'; -import type { PackageJson } from './project-info'; -import type { Mutable } from './utils'; - -export const TYPES_COMPAT = '.types-compat'; - -const LOG = log4js.getLogger('jsii/compiler'); - -const TS_VERSION = new 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: readonly SemVer[] = ['3.9'].map((ver) => new SemVer(`${ver}.0`)); - -/** - * Produces down-leveled declaration files to ensure compatibility with previous - * compiler releases (matching 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` - */ -export function emitDownleveledDeclarations(projectRoot: string, packageJson: PackageJson, outDir?: string) { - const compatRoot = join(projectRoot, ...(outDir != null ? [outDir] : []), TYPES_COMPAT); - rmSync(compatRoot, { force: true, recursive: true }); - - const rewrites = new Set<`${number}.${number}`>(); - - 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 = mkdtempSync(join(tmpdir(), `downlevel-dts-${breakpoint}-${basename(projectRoot)}-`)); - try { - downlevel(projectRoot, workdir, breakpoint.version); - const projectOutDir = outDir != null ? join(projectRoot, outDir) : projectRoot; - const workOutDir = outDir != null ? join(workdir, outDir) : workdir; - for (const dts of walkDirectory(workOutDir)) { - const original = readFileSync(join(projectOutDir, dts), 'utf-8'); - const downleveledPath = join(workOutDir, dts); - const downleveled = readFileSync(downleveledPath, 'utf-8'); - 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 = join(compatRoot, versionSuffix); - if (!existsSync(compatDir)) { - mkdirSync(compatDir, { recursive: true }); - try { - // Write an empty .npmignore file so that npm pack doesn't use the .gitignore file... - writeFileSync(join(compatRoot, '.npmignore'), '\n', 'utf-8'); - // Make sure all of this is gitignored, out of courtesy... - writeFileSync(join(compatRoot, '.gitignore'), '*\n', 'utf-8'); - } catch { - // Ignore any error here... This is inconsequential. - } - } - - for (const [dts, downleveledPath] of rewriteSet) { - const rewritten = join(compatDir, dts); - // Make sure the parent directory exists (dts might be nested) - mkdirSync(dirname(rewritten), { recursive: true }); - // Write the re-written declarations file there... - copyFileSync(downleveledPath, rewritten); - } - } - } catch (error) { - LOG.error(error); - } finally { - // Clean up after ourselves... - rmSync(workdir, { force: true, recursive: true }); - } - } - - let typesVersions: Mutable; - - for (const version of rewrites) { - // Register the type redirect in the typesVersions configuration - typesVersions ??= {}; - const from = [...(outDir != null ? [outDir] : []), '*'].join('/'); - const to = [...(outDir != null ? [outDir] : []), 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 as any; - } - } else { - obj[key] = value as any; - // 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 as any; - } - } - return obj; - }, {} as Mutable); - // 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 as any; - } - - const packageJsonFile = join(projectRoot, 'package.json'); - - // We try "hard" to preserve the existing indent in the `package.json` file when updating it. - const [, indent] = readFileSync(packageJsonFile, 'utf-8').match(/^(\s*)"/m) ?? [null, 2]; - - writeFileSync(packageJsonFile, `${JSON.stringify(newPackageJson, undefined, indent)}\n`, 'utf-8'); -} - -/** - * 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: string, right: string): boolean { - // We normalize declarations largely by parsing & re-printing them. - const normalizeDeclarations = (code: string): string => { - 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 = '/// '; - 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: string, root: string = dir): Generator { - for (const file of readdirSync(dir)) { - const filePath = join(dir, file); - if (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 relative(root, filePath); - } - } -} diff --git a/test/compiler.test.ts b/test/compiler.test.ts index 26036491..96d001a6 100644 --- a/test/compiler.test.ts +++ b/test/compiler.test.ts @@ -3,8 +3,7 @@ import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { loadAssemblyFromPath, SPEC_FILE_NAME, SPEC_FILE_NAME_COMPRESSED } from '@jsii/spec'; import { compile, Lock } from './fixtures'; -import { Compiler } from '../src/compiler'; -import { TYPES_COMPAT } from '../src/downlevel-dts'; +import { Compiler, TYPES_COMPAT } from '../src/compiler'; import { ProjectInfo } from '../src/project-info'; import { TypeScriptConfigValidationRuleSet } from '../src/tsconfig'; import { TypeScriptConfigValidator } from '../src/tsconfig/tsconfig-validator'; diff --git a/test/downlevel.test.ts b/test/downlevel.test.ts deleted file mode 100644 index 81e0c373..00000000 --- a/test/downlevel.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { mkdtempSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { Compiler } from '../src/compiler'; -import { TYPES_COMPAT } from '../src/downlevel-dts'; -import { PackageJson, ProjectInfo } from '../src/project-info'; - -describe('Compiler', () => { - describe('generated tsconfig', () => { - test('will downlevel code', () => { - // GIVEN - const sourceDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-generated-')); - _writeDownlevelableCode(sourceDir); - - // WHEN - const compiler = new Compiler({ - projectInfo: { - ..._makeProjectInfo(sourceDir, 'index.d.ts'), - }, - }); - const result = compiler.emit(); - - // THEN code compiles - expect(result.diagnostics).toHaveLength(0); - expect(result.emitSkipped).toBe(false); - // THEN code is downleveled - const downleveled = readFileSync(join(sourceDir, '.types-compat/ts3.9/index.d.ts'), 'utf-8'); - expect(downleveled).toMatchInlineSnapshot(` - "declare class MarkerA { - } - export type { MarkerA }; - " - `); - // THEN typeVersions are written - const packageJson = readPackageJson(sourceDir); - expect(packageJson.typesVersions).toMatchObject({ - '<=3.9': { - '*': ['.types-compat/ts3.9/*', '.types-compat/ts3.9/*/index.d.ts'], - }, - }); - // THEN - const tsconfig = JSON.parse(readFileSync(join(sourceDir, 'tsconfig.json'), 'utf-8')); - expect(tsconfig.exclude).toMatchInlineSnapshot(` - [ - "node_modules", - ".types-compat", - ] - `); - }); - - test('will downlevel code with outdir', () => { - // GIVEN - const outDir = 'jsii-outdir'; - const srcDir = 'jsii-srcdir'; - const projectDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-generated-')); - mkdirSync(join(projectDir, srcDir), { recursive: true }); - _writeDownlevelableCode(projectDir, srcDir); - - // WHEN - const compiler = new Compiler({ - projectInfo: { - ..._makeProjectInfo(projectDir, join(outDir, 'index.d.ts')), - tsc: { - outDir, - rootDir: srcDir, - }, - }, - }); - const result = compiler.emit(); - - // THEN code compiles - expect(result.diagnostics).toHaveLength(0); - expect(result.emitSkipped).toBe(false); - // THEN code is downleveled - const downleveled = readFileSync(join(projectDir, outDir, '.types-compat/ts3.9/index.d.ts'), 'utf-8'); - expect(downleveled).toMatchInlineSnapshot(` - "declare class MarkerA { - } - export type { MarkerA }; - " - `); - // THEN typeVersions are written - const packageJson = readPackageJson(projectDir); - expect(packageJson.typesVersions).toMatchObject({ - '<=3.9': { - 'jsii-outdir/*': ['jsii-outdir/.types-compat/ts3.9/*', 'jsii-outdir/.types-compat/ts3.9/*/index.d.ts'], - }, - }); - // THEN - const tsconfig = JSON.parse(readFileSync(join(projectDir, 'tsconfig.json'), 'utf-8')); - expect(tsconfig.exclude).toMatchInlineSnapshot(` - [ - "node_modules", - "jsii-outdir/.types-compat", - ] - `); - }); - }); - - describe('user-provided tsconfig', () => { - test('will downlevel code with outdir', () => { - // GIVEN - const outDir = 'jsii-outdir'; - const srcDir = 'jsii-srcdir'; - const projectDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-user-tsconfig-')); - mkdirSync(join(projectDir, srcDir), { recursive: true }); - const tsconfigPath = 'tsconfig.dev.json'; - writeFileSync( - join(projectDir, tsconfigPath), - JSON.stringify( - tsconfigForNode18Strict({ - outDir, - rootDir: srcDir, - }), - null, - 2, - ), - ); - _writeDownlevelableCode(projectDir, srcDir); - - // WHEN - const compiler = new Compiler({ - projectInfo: _makeProjectInfo(projectDir, join(outDir, 'index.d.ts')), - typeScriptConfig: tsconfigPath, - }); - const result = compiler.emit(); - - // THEN code compiles - expect(result.diagnostics).toHaveLength(0); - expect(result.emitSkipped).toBe(false); - // THEN code is downleveled - const downleveled = readFileSync(join(projectDir, outDir, '.types-compat/ts3.9/index.d.ts'), 'utf-8'); - expect(downleveled).toMatchInlineSnapshot(` - "declare class MarkerA { - } - export type { MarkerA }; - " - `); - // THEN typeVersions are written - const packageJson = readPackageJson(projectDir); - expect(packageJson.typesVersions).toMatchObject({ - '<=3.9': { - 'jsii-outdir/*': ['jsii-outdir/.types-compat/ts3.9/*', 'jsii-outdir/.types-compat/ts3.9/*/index.d.ts'], - }, - }); - }); - }); -}); - -function readPackageJson(dir: string): PackageJson { - return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8')); -} - -function _writeDownlevelableCode(projectDir: string, codeSubDir?: string) { - // Files in the project dir - writeFileSync(join(projectDir, 'README.md'), '# Test Package'); - writeFileSync(join(projectDir, 'package.json'), JSON.stringify({}, null, 2)); - - // Files in the code dir, e.g. `src` - const codeDir = codeSubDir ? join(projectDir, codeSubDir) : projectDir; - // See https://www.npmjs.com/package/downlevel-dts#type-modifiers-on-importexport-names-45 - writeFileSync(join(codeDir, 'index.ts'), 'class MarkerA {} export { type MarkerA }'); -} - -function _makeProjectInfo(sourceDir: string, types: string): ProjectInfo { - return { - projectRoot: sourceDir, - description: 'test', - homepage: 'https://github.com/aws/jsii-compiler', - packageJson: {}, - types, - main: types.replace(/(?:\.d)?\.ts(x?)/, '.js$1'), - name: 'jsii', // That's what package.json would tell if we look up... - version: '0.0.1', - jsiiVersionFormat: 'short', - license: 'Apache-2.0', - author: { name: 'John Doe', roles: ['author'] }, - repository: { type: 'git', url: 'https://github.com/aws/jsii.git' }, - dependencies: {}, - peerDependencies: {}, - dependencyClosure: [], - bundleDependencies: {}, - targets: {}, - excludeTypescript: [], - tsc: { - // NOTE: these are the default values jsii uses when none are provided in package.json. - inlineSourceMap: true, - inlineSources: true, - }, - }; -} - -/** - * An example of a user-provided config, based on the popular tsconfig/bases project & adjusted for the strict rule set - * @see https://github.com/tsconfig/bases/blob/main/bases/node18.json - */ -function tsconfigForNode18Strict(compilerOptions: any = {}) { - return { - compilerOptions: { - lib: ['es2022'], - module: 'node16', - target: 'es2022', - - strict: true, - esModuleInterop: true, - skipLibCheck: true, - noEmitOnError: true, - moduleResolution: 'node16', - declaration: true, - ...compilerOptions, - }, - exclude: ['node_modules', TYPES_COMPAT], - include: [join('**', '*.ts')], - }; -} diff --git a/yarn.lock b/yarn.lock index f77ada50..8553b22f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6439,9 +6439,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@next: - version "5.8.0-dev.20241110" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.0-dev.20241110.tgz#0bf464a4d3389b3fe8bf8afe30769ca45ae7105b" - integrity sha512-0OdrDNPQVyHr/LRSAFbGnuI1a+J72vMFrAsEN2ge8MhW9eihGcqodQrkllBqBLt7dqFUexbK2qOxFun1SUFtQA== + version "5.8.0-dev.20241111" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.0-dev.20241111.tgz#8a481638472882e0c0bb6faec641a71b1858c76c" + integrity sha512-deGbPJipGG/DiJ4nR0NhFuccD2XD6xfUAQCvqMLWxVsM4p9zmhKNMWotkMQEm/l9HjXhnVr0dFJ8u4B9y3y+ZA== typescript@~3.9.10: version "3.9.10"