Skip to content

Commit

Permalink
fix: lib setting from custom config is ignored (#1576)
Browse files Browse the repository at this point in the history
When a user provides a custom `tsconfig.json` file, they have the
ability to override the `lib` setting, which provides a number of
libraries that should be loaded at compile time.

This config setting is not passed directly into the TypeScript compiler.
Instead, it is resolved and translated into a list of `.d.ts` file that
are passed as `rootNames` to the compiler, along with the actual source
files.

In that resolution, we failed to look at the `lib` setting the user
provided, and instead always used the default config.

Fix by looking at the user-provided config, falling back to the default
config only if the user config doesn't have a `lib` setting (mirroring
the behavior of tsc).

Fixes aws/jsii#4706.

---

By submitting this pull request, I confirm that my contribution is made
under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0

(cherry picked from commit 26a3d96)
  • Loading branch information
rix0rrr committed Dec 13, 2024
1 parent e0f3508 commit 083cbe1
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 16 deletions.
20 changes: 11 additions & 9 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class Compiler implements Emitter {
const tsconf = this.tsconfig!;

const prog = ts.createIncrementalProgram({
rootNames: this.rootFiles.concat(_pathOfLibraries(this.compilerHost)),
rootNames: this.rootFiles.concat(_pathOfLibraries(tsconf.compilerOptions, this.compilerHost)),
options: tsconf.compilerOptions,
// Make the references absolute for the compiler
projectReferences: tsconf.references?.map((ref) => ({
Expand Down Expand Up @@ -600,17 +600,19 @@ export interface NonBlockingWatchOptions {
readonly compilationComplete: (emitResult: ts.EmitResult) => void;
}

function _pathOfLibraries(host: ts.CompilerHost | ts.WatchCompilerHost<any>): string[] {
if (!BASE_COMPILER_OPTIONS.lib || BASE_COMPILER_OPTIONS.lib.length === 0) {
function _pathOfLibraries(options: ts.CompilerOptions, host: ts.CompilerHost | ts.WatchCompilerHost<any>): string[] {
// Prefer user libraries, falling back to a library based on the target if not supplied by the user.
// This matches tsc behavior.
const libs = options.lib ?? [ts.getDefaultLibFileName(options)] ?? [];
if (libs.length === 0) {
return [];
}
const lib = host.getDefaultLibLocation?.();
if (!lib) {
throw new Error(
`Compiler host doesn't have a default library directory available for ${BASE_COMPILER_OPTIONS.lib.join(', ')}`,
);

const libDir = host.getDefaultLibLocation?.();
if (!libDir) {
throw new Error(`Compiler host doesn't have a default library directory available for ${libs.join(', ')}`);
}
return BASE_COMPILER_OPTIONS.lib.map((name) => path.join(lib, name));
return libs.map((name) => path.join(libDir, name));
}

function parseConfigHostFromCompilerHost(host: ts.CompilerHost): ts.ParseConfigHost {
Expand Down
68 changes: 61 additions & 7 deletions test/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { mkdirSync, existsSync, mkdtempSync, rmSync, writeFileSync, readFileSync
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { loadAssemblyFromPath, SPEC_FILE_NAME, SPEC_FILE_NAME_COMPRESSED } from '@jsii/spec';
import * as ts from 'typescript';
import { compile, Lock } from './fixtures';
import { Compiler } from '../src/compiler';
import { TYPES_COMPAT } from '../src/downlevel-dts';
import { ProjectInfo } from '../src/project-info';
import { TypeScriptConfigValidationRuleSet } from '../src/tsconfig';
import { TypeScriptConfigValidator } from '../src/tsconfig/tsconfig-validator';

// This is necessary to be able to jest.spyOn to functions in the 'ts' module. Replace the read-only
// object descriptors with a plain object.
jest.mock('typescript', () => ({ ...jest.requireActual('typescript') }));

beforeEach(() => {
jest.restoreAllMocks();
});

describe(Compiler, () => {
describe('generated tsconfig', () => {
test('default is tsconfig.json', () => {
Expand Down Expand Up @@ -227,9 +236,17 @@ describe(Compiler, () => {
});

describe('user-provided tsconfig', () => {
let sourceDir: string;
const tsconfigPath = 'tsconfig.dev.json';
beforeEach(() => {
sourceDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-user-tsconfig-'));
});

afterEach(() => {
rmSync(sourceDir, { force: true, recursive: true });
});

test('will use user-provided config', () => {
const sourceDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-user-tsconfig-'));
const tsconfigPath = 'tsconfig.dev.json';
writeFileSync(join(sourceDir, tsconfigPath), JSON.stringify(tsconfigForNode18Strict(), null, 2));

writeFileSync(join(sourceDir, 'index.ts'), 'export class MarkerA {}');
Expand All @@ -245,9 +262,7 @@ describe(Compiler, () => {
});

test('use user-provided config uses include and exclude', () => {
const sourceDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-user-tsconfig-'));
mkdirSync(join(sourceDir, 'sub'));
const tsconfigPath = 'tsconfig.dev.json';
writeFileSync(
join(sourceDir, tsconfigPath),
JSON.stringify(
Expand Down Expand Up @@ -282,9 +297,48 @@ describe(Compiler, () => {
expect(result.emitSkipped).toBe(false);
});

test('respect "lib" setting from user-provided config', () => {
const tsconfig = tsconfigForNode18Strict();
tsconfig.compilerOptions.lib = ['Decorators.Legacy']; // Something very nonstandard
writeFileSync(join(sourceDir, tsconfigPath), JSON.stringify(tsconfig, null, 2));

const createIncrementalProgram = jest.spyOn(ts, 'createIncrementalProgram');

const compiler = new Compiler({
projectInfo: _makeProjectInfo(sourceDir, 'index.d.ts'),
typeScriptConfig: tsconfigPath,
});
compiler.emit();

expect(createIncrementalProgram).toHaveBeenCalledWith(
expect.objectContaining({
rootNames: expect.arrayContaining([expect.stringContaining('lib.decorators.legacy.d.ts')]),
}),
);
});

test('missing "lib" setting is based on compilation target', () => {
const tsconfig = tsconfigForNode18Strict();
tsconfig.compilerOptions.target = 'es6';
delete tsconfig.compilerOptions.lib;
writeFileSync(join(sourceDir, tsconfigPath), JSON.stringify(tsconfig, null, 2));

const createIncrementalProgram = jest.spyOn(ts, 'createIncrementalProgram');

const compiler = new Compiler({
projectInfo: _makeProjectInfo(sourceDir, 'index.d.ts'),
typeScriptConfig: tsconfigPath,
});
compiler.emit();

expect(createIncrementalProgram).toHaveBeenCalledWith(
expect.objectContaining({
rootNames: expect.arrayContaining([expect.stringContaining('lib.es6.d.ts')]),
}),
);
});

test('"watch" mode', async () => {
const sourceDir = mkdtempSync(join(tmpdir(), 'jsii-compiler-watch-mode-'));
const tsconfigPath = 'tsconfig.dev.json';
writeFileSync(join(sourceDir, tsconfigPath), JSON.stringify(tsconfigForNode18Strict(), null, 2));

try {
Expand Down Expand Up @@ -465,7 +519,7 @@ function expectedTypeScriptConfig() {
function tsconfigForNode18Strict() {
return {
compilerOptions: {
lib: ['es2022'],
lib: ['es2022'] as string[] | undefined,
module: 'node16',
target: 'es2022',

Expand Down

0 comments on commit 083cbe1

Please sign in to comment.