Skip to content

Commit

Permalink
feature(type-compiler): use TS config resolution code to resolve refl…
Browse files Browse the repository at this point in the history
…ection options.

Also added glob feature in reflection, example:

    "reflection": ["!frontend/**/*.ts", "backend/**/*.ts"]

Also print more debugging information when DEBUG=deepkit env variable is set.

fixes #436
fixes #394
  • Loading branch information
marcj committed Apr 16, 2023
1 parent 48de497 commit bb2ac7e
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 36 deletions.
113 changes: 77 additions & 36 deletions packages/type-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spe
import { Resolver } from './resolver.js';
import { knownLibFilesForCompilerOptions } from '@typescript/vfs';
import { contains } from 'micromatch';
import { parseTsconfig } from 'get-tsconfig';
import { isObject } from '@deepkit/core';

const {
visitEachChild,
Expand Down Expand Up @@ -530,31 +530,25 @@ export class ReflectionTransformer implements CustomTransformer {
protected compilerOptions: CompilerOptions;

/**
* When an deep call expression was found a script-wide variable is necessary
* When a deep call expression was found a script-wide variable is necessary
* as temporary storage.
*/
protected tempResultIdentifier?: Identifier;

protected config: { compilerOptions: ts.CompilerOptions, extends?: string, reflectionOptions?: ReflectionOptions, reflection?: string | string[] } = { compilerOptions: {} };

constructor(
protected context: TransformationContext,
) {
this.f = context.factory;
this.nodeConverter = new NodeConverter(this.f);
this.compilerOptions = context.getCompilerOptions();
//some builder do not provide the full compiler options (e.g. webpack in nx),
//so we need to load the file manually and apply what we need.
if ('string' === typeof this.compilerOptions.configFilePath && !this.compilerOptions.paths) {
const resolved = parseTsconfig(this.compilerOptions.configFilePath);
if (resolved.compilerOptions) {
this.compilerOptions.paths = resolved.compilerOptions.paths;
}
}

this.host = createCompilerHost(this.compilerOptions);
this.resolver = new Resolver(this.compilerOptions, this.host);
}

forHost(host: CompilerHost): this {
this.host = host;
this.resolver.host = host;
return this;
}
Expand Down Expand Up @@ -592,21 +586,63 @@ export class ReflectionTransformer implements CustomTransformer {
(sourceFile as any).deepkitTransformed = true;
this.embedAssignType = false;


if (!(sourceFile as any).locals) {
//@ts-ignore
ts.bindSourceFile(sourceFile, this.compilerOptions);
//some builder do not provide the full compiler options (e.g. webpack in nx),
//so we need to load the file manually and apply what we need.
if ('string' === typeof this.compilerOptions.configFilePath) {
const configFile = ts.readConfigFile(this.compilerOptions.configFilePath, (path: string) => this.host.readFile(path));
if (configFile) {
this.config = Object.assign({ compilerOptions: {} }, configFile.config);
this.compilerOptions = Object.assign(this.config.compilerOptions, this.compilerOptions);
}
} else {
//find tsconfig via sourceFile.fileName
const configPath = ts.findConfigFile(dirname(sourceFile.fileName), (path) => this.host.fileExists(path));
if (configPath) {
const configFile = ts.readConfigFile(configPath, (path: string) => this.host.readFile(path));
if (configFile) {
this.config = Object.assign({ compilerOptions: {} }, configFile.config);
this.compilerOptions = Object.assign(this.config.compilerOptions, this.compilerOptions);
this.compilerOptions.configFilePath = configPath;
}
}
}

this.host = createCompilerHost(this.compilerOptions);
this.resolver = new Resolver(this.compilerOptions, this.host);

this.addImports = [];
this.sourceFile = sourceFile;

const reflection = this.findReflectionConfig(sourceFile);
this.currentReflectionConfig = reflection;
if (reflection.mode === 'never') {
//iterate through all configs (this.config.extends) until we have all reflection options found.
let currentConfig = this.config;
let basePath = this.config.compilerOptions.configFilePath as string;
if (basePath) {
basePath = dirname(basePath);
debugger;
if (!this.reflectionMode && currentConfig.reflection !== undefined) this.reflectionMode = this.parseReflectionMode(currentConfig.reflection, basePath);
if (!this.compilerOptions && currentConfig.reflectionOptions !== undefined) this.reflectionOptions = this.parseReflectionOptionsDefaults(currentConfig.reflectionOptions);
while ((this.reflectionMode === undefined || this.compilerOptions === undefined) && 'string' === typeof basePath && currentConfig.extends) {
const path = join(basePath, currentConfig.extends);
const nextConfig = ts.readConfigFile(path, (path: string) => this.host.readFile(path));
if (!nextConfig) break;
if (!this.reflectionMode && nextConfig.config.reflection !== undefined) this.reflectionMode = this.parseReflectionMode(nextConfig.config.reflection, basePath);
if (!this.reflectionOptions && nextConfig.config.reflectionOptions !== undefined) this.reflectionOptions = this.parseReflectionOptionsDefaults(nextConfig.config.reflectionOptions);
currentConfig = Object.assign({}, nextConfig.config);
basePath = dirname(path);
}
}

debug(`Transform file ${sourceFile.fileName} via config ${this.compilerOptions.configFilePath || 'none'}, reflection=${this.reflectionMode}.`);

if (this.reflectionMode === 'never') {
return sourceFile;
}

if (!(sourceFile as any).locals) {
//@ts-ignore
ts.bindSourceFile(sourceFile, this.compilerOptions);
}

if (sourceFile.kind !== SyntaxKind.SourceFile) {
const path = require.resolve('typescript');
throw new Error(`Invalid TypeScript library imported. SyntaxKind different ${sourceFile.kind} !== ${SyntaxKind.SourceFile}. typescript package path: ${path}`);
Expand Down Expand Up @@ -2469,25 +2505,25 @@ export class ReflectionTransformer implements CustomTransformer {
);
}

protected parseReflectionMode(mode?: typeof reflectionModes[number] | '' | boolean | string[], configPathDir?: string): typeof reflectionModes[number] {
protected parseReflectionMode(mode: typeof reflectionModes[number] | '' | boolean | string | string[] | undefined, configPathDir: string): typeof reflectionModes[number] {
if (Array.isArray(mode)) {
if (!configPathDir) return 'never';
if (this.sourceFile.fileName.startsWith(configPathDir)) {
const fileName = this.sourceFile.fileName.slice(configPathDir.length + 1);
for (const entry of mode) {
if (entry === fileName) return 'default';
}
}
return 'never';
const matches = contains(this.sourceFile.fileName, mode, {
cwd: configPathDir
});

return matches ? 'default' : 'never';
}
if ('boolean' === typeof mode) return mode ? 'default' : 'never';
return mode || 'never';
if (mode === 'default' || mode === 'always') return mode;
return 'never';
}

protected resolvedTsConfig: { [path: string]: { data: Record<string, any>, exists: boolean } } = {};
protected resolvedPackageJson: { [path: string]: { data: Record<string, any>, exists: boolean } } = {};

protected applyReflectionOptionsDefaults(options: ReflectionOptions) {
protected parseReflectionOptionsDefaults(options: ReflectionOptions) {
options = isObject(options) ? options : {};
if (!options.exclude) options.exclude = this.defaultExcluded;
return options;
}
Expand All @@ -2496,7 +2532,7 @@ export class ReflectionTransformer implements CustomTransformer {
if (program && program.sourceFile.fileName !== this.sourceFile.fileName) {
//when the node is from another module it was already decided that it will be reflected, so
//make sure it returns correct mode. for globals this would read otherwise to `mode: never`.
return { mode: 'always', options: this.applyReflectionOptionsDefaults({}) };
return { mode: 'always', options: this.parseReflectionOptionsDefaults({}) };
}

let current: Node | undefined = node;
Expand All @@ -2506,17 +2542,22 @@ export class ReflectionTransformer implements CustomTransformer {
const tags = getJSDocTags(current);
for (const tag of tags) {
if (!reflection && getIdentifierName(tag.tagName) === 'reflection' && 'string' === typeof tag.comment) {
return { mode: this.parseReflectionMode(tag.comment as any || true), options: this.applyReflectionOptionsDefaults({}) };
return { mode: this.parseReflectionMode(tag.comment as any || true, ''), options: this.parseReflectionOptionsDefaults({}) };
}
}
current = current.parent;
} while (current);

//nothing found, look in tsconfig.json
if (this.reflectionMode !== undefined) return { mode: this.reflectionMode, options: this.applyReflectionOptionsDefaults(this.reflectionOptions || {}) };
if (this.reflectionMode !== undefined) return { mode: this.reflectionMode, options: this.parseReflectionOptionsDefaults(this.reflectionOptions || {}) };

if (!serverEnv) {
return { mode: 'default', options: this.applyReflectionOptionsDefaults({}) };
return { mode: 'default', options: this.parseReflectionOptionsDefaults({}) };
}

if (program && program.sourceFile.fileName === this.sourceFile.fileName) {
//if the node is from the same module as we currently process, we use the loaded reflection options set early in transformSourceFile().
return { mode: this.reflectionMode || 'never', options: this.parseReflectionOptionsDefaults(this.reflectionOptions || {}) };
}

const sourceFile = findSourceFile(node) || this.sourceFile;
Expand All @@ -2536,7 +2577,7 @@ export class ReflectionTransformer implements CustomTransformer {

protected findReflectionFromPath(path: string): ReflectionConfig {
if (!serverEnv) {
return { mode: 'default', options: this.applyReflectionOptionsDefaults({}) };
return { mode: 'default', options: this.parseReflectionOptionsDefaults({}) };
}

let currentDir = dirname(path);
Expand Down Expand Up @@ -2602,15 +2643,15 @@ export class ReflectionTransformer implements CustomTransformer {
return {
mode: this.parseReflectionMode(packageJson.reflection, currentDir),
baseDir: currentDir,
options: this.applyReflectionOptionsDefaults(packageJson.reflectionOptions || {})
options: this.parseReflectionOptionsDefaults(packageJson.reflectionOptions || {})
};
}

if (reflection === undefined && tsConfig.reflection !== undefined) {
return {
mode: this.parseReflectionMode(tsConfig.reflection, currentDir),
baseDir: currentDir,
options: this.applyReflectionOptionsDefaults(tsConfig.reflectionOptions || {})
options: this.parseReflectionOptionsDefaults(tsConfig.reflectionOptions || {})
};
}

Expand All @@ -2625,7 +2666,7 @@ export class ReflectionTransformer implements CustomTransformer {
currentDir = next;
}

return { mode: reflection || 'never', options: this.applyReflectionOptionsDefaults({}) };
return { mode: reflection || 'never', options: this.parseReflectionOptionsDefaults({}) };
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/type-compiler/tests/setup/suite1/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {WithTypes} from './file1';
import {WithoutTypesFrontend} from './frontend/file2';
import { WithTypesBackend } from './backend/file3';

console.log(WithTypes, WithoutTypesFrontend, WithTypesBackend);
3 changes: 3 additions & 0 deletions packages/type-compiler/tests/setup/suite1/backend/file3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class WithTypesBackend {
name!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"files": [
"file3.ts"
]
}
3 changes: 3 additions & 0 deletions packages/type-compiler/tests/setup/suite1/file1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class WithTypes {
name!: string;
}
3 changes: 3 additions & 0 deletions packages/type-compiler/tests/setup/suite1/frontend/file2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class WithoutTypesFrontend {
name!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"reflection": false,
"files": [
"file2.ts"
]
}
13 changes: 13 additions & 0 deletions packages/type-compiler/tests/setup/suite1/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"strict": true,
"moduleResolution": "node",
"target": "ES2020",
"module": "CommonJS",
"esModuleInterop": true
},
"reflection": true,
"files": [
"app.ts"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"reflection": [
"**/*/file1.ts"
]
}
52 changes: 52 additions & 0 deletions packages/type-compiler/tests/setup/suits.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test, expect } from '@jest/globals';
import * as ts from 'typescript';
import { TransformationContext } from 'typescript';
import { ReflectionTransformer } from '../../src/compiler.js';

function build(currentDir = process.cwd(), useConfig = 'tsconfig.json'): { [path: string]: string } {
process.env.DEBUG = 'deepkit';
const configFile = ts.findConfigFile(currentDir, ts.sys.fileExists, useConfig);
if (!configFile) throw Error('tsconfig.json not found');
const { config } = ts.readConfigFile(configFile, ts.sys.readFile);

const { options, fileNames, errors } = ts.parseJsonConfigFileContent(config, ts.sys, currentDir);
options.configFilePath = configFile;

const program = ts.createProgram({ options, rootNames: fileNames, configFileParsingDiagnostics: errors });

const result: { [path: string]: string } = {};
program.emit(undefined, (fileName, data) => {
//add basename to currentDir from fileName to result
result[fileName.slice(currentDir.length + 1).replace(/\.js$/, '')] = data;
}, undefined, undefined, {
before: [(context: TransformationContext) => new ReflectionTransformer(context)],
});

return result;
}

test('suite1 base default', async () => {
const files = build(__dirname + '/suite1');
expect(files['file1']).toContain('WithTypes.__type');
expect(files['backend/file3']).toContain('WithTypesBackend.__type');
//frontend contains types because frontend/tsconfig.json is not picked.
expect(files['frontend/file2']).toContain('WithoutTypesFrontend.__type');
});

test('suite1 base no-types', async () => {
const files = build(__dirname + '/suite1', 'tsconfig.no-types.json');
expect(files['file1']).toContain('WithTypes.__type');
expect(files['backend/file3']).not.toContain('WithTypesBackend.__type');
//frontend contains types because frontend/tsconfig.json is not picked.
expect(files['frontend/file2']).not.toContain('WithoutTypesFrontend.__type');
});

test('suite1 frontend', async () => {
const files = build(__dirname + '/suite1/frontend');
expect(files.file2).not.toContain('WithoutTypesFrontend.__type');
});

test('suite1 backend', async () => {
const files = build(__dirname + '/suite1/backend');
expect(files.file3).toContain('WithTypesBackend.__type');
});

0 comments on commit bb2ac7e

Please sign in to comment.