I achieved exactly this using the compiler API. This is a (greatly) modifed
version of their minimal compiler example.
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
import * as cp from 'child_process';
import * as dm from '../src-ts/deep-merge-objects';
function compile(fileNames: string[], options: ts.CompilerOptions): void {
const program = ts.createProgram(fileNames, options);
const emitResult = program.emit();
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(
diagnostic.start!
);
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
);
console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
} else {
console.log(
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
);
}
});
const exitCode = emitResult.emitSkipped ? 1 : 0;
console.log(`Process exiting with code '${exitCode}'.`);
process.exit(exitCode);
}
function main(): void {
fs.mkdirSync('./dev-out',{recursive:true})
cp.execSync('cp -r ./testlib-js ./dev-out/');
const tscfgFilenames = [
'@tsconfig/node14/tsconfig.json',
path.join(process.cwd(), 'tsconfig.base.json'),
path.join(process.cwd(), 'tsconfig.json'),
];
const tscfg = tscfgFilenames.map((fn) => require(fn).compilerOptions);
const compilerOptions: ts.CompilerOptions = dm.deepMerge(
dm.deepMergeInnerDedupeArrays,
...tscfg
);
if (compilerOptions.lib && compilerOptions.lib.length)
compilerOptions.lib = compilerOptions.lib.map((s) => 'lib.' + s + '.d.ts');
console.log(JSON.stringify(compilerOptions, null, 2));
compile(process.argv.slice(2), compilerOptions);
}
try {
main();
if (process.exitCode === 1) console.log('compiler produced no output');
} catch (e) {
console.error(e.message);
process.exitCode = 2;
}
The gotcha's were
- Reading in the config file(s). That was not included in the original example.
- Merging the config files in order. (Not a problem if you use a single file).
- Transforming the
compilerOptions.lib
entries
if (compilerOptions.lib && compilerOptions.lib.length)
compilerOptions.lib = compilerOptions.lib.map((s) => 'lib.' + s + '.d.ts');
E.g., "es2020" is changed to "lib.es2020.d.ts".
The generic object merging code is here:
// adapted from adrian-marcelo-gallardo
// https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6#gistcomment-3257606
//
type objectType = Record<string, any>;
export const isObject = (obj: unknown): obj is objectType => {
return <boolean>obj && typeof obj === 'object';
};
export function deepMerge(
deepMergeInner: (target: objectType, source: objectType) => objectType,
...objects: objectType[]
): objectType {
if (objects.length === 0) return {};
if (objects.length === 1) return objects[0];
if (objects.some((object) => !isObject(object))) {
throw new Error('deepMerge: all values should be of type "object"');
}
const target = objects.shift() as objectType;
//console.log(JSON.stringify(target,null,2))
let source: objectType;
while ((source = objects.shift() as objectType)) {
deepMergeInner(target, source);
//console.log(JSON.stringify(target,null,2))
}
return target;
}
export function deepMergeInnerDedupeArrays(
target: objectType,
source: objectType
): objectType {
function uniquify(a: any[]): any[] {
return a.filter((v, i) => a.indexOf(v) === i);
}
Object.keys(source).forEach((key: string) => {
const targetValue = target[key];
const sourceValue = source[key];
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = uniquify(targetValue.concat(sourceValue));
} else if (isObject(targetValue) && Array.isArray(sourceValue)) {
target[key] = sourceValue;
} else if (Array.isArray(targetValue) && isObject(sourceValue)) {
target[key] = sourceValue;
} else if (isObject(targetValue) && isObject(sourceValue)) {
target[key] = deepMergeInnerDedupeArrays(
Object.assign({}, targetValue),
sourceValue
);
} else {
target[key] = sourceValue;
}
});
return target;
}
Another this is directory structure. You might not get the same output directory as your get with all files unless you use
compilerOptions.rootDir
. Typically:
"compilerOptions": {
"outDir": "dev-out",
"rootDir": "./"
},
Read about rootDir here.