Skip to content

Commit acb2d72

Browse files
committed
fix(type-compiler): resolve reflection mode paths correctly
previously complex reflection options like ``` reflection: [ 'src/ts/**/*.ts', '!src/ts/exclude-this.ts' ] ``` did not work. this works now Also add a new script `deepkit-compiler-debug` to allow transpile a single typescript file and see its transformed output directly. Also include a plugin function that can be used in custom build systems more easily
1 parent 5870005 commit acb2d72

File tree

9 files changed

+190
-40
lines changed

9 files changed

+190
-40
lines changed
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { deepkitType } from './src/plugin.js';
2+
import { readFileSync } from 'fs';
3+
4+
interface Arguments {
5+
file: string;
6+
config?: string;
7+
}
8+
9+
function parseArguments(args: string[]): Arguments {
10+
const result: Arguments = {
11+
file: '',
12+
};
13+
14+
for (let i = 0; i < args.length; i++) {
15+
const arg = args[i];
16+
if (arg === '--config') {
17+
result.config = args[i + 1];
18+
i++;
19+
} else {
20+
result.file = arg;
21+
}
22+
}
23+
24+
return result;
25+
}
26+
27+
const args = parseArguments(process.argv.slice(2));
28+
29+
30+
31+
const transformer = deepkitType({
32+
tsConfig: args.config,
33+
});
34+
35+
const code = readFileSync(args.file, 'utf8');
36+
const transformed = transformer(code, args.file);
37+
38+
console.log(transformed?.code);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
3+
try {
4+
require('./dist/cjs/compiler-debug.js');
5+
} catch (error) {}

packages/type-compiler/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { declarationTransformer, transformer } from './src/compiler.js';
1212
import type { Program } from 'typescript';
1313

1414
export * from './src/compiler.js';
15+
export * from './src/plugin.js';
1516
export * from './src/loader.js';
1617

1718
export default function myTransformerPlugin(program: Program, opts: {}) {

packages/type-compiler/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
}
1515
},
1616
"bin": {
17-
"deepkit-type-install": "./deepkit-type-install.js"
17+
"deepkit-type-install": "./deepkit-type-install.js",
18+
"deepkit-compiler-debug": "./deepkit-compiler-debug.js"
1819
},
1920
"sideEffects": false,
2021
"publishConfig": {

packages/type-compiler/src/compiler.ts

+19-29
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,8 @@ import { existsSync, readFileSync } from 'fs';
9090
import { dirname, isAbsolute, join, resolve } from 'path';
9191
import stripJsonComments from 'strip-json-comments';
9292
import { MappedModifier, ReflectionOp, TypeNumberBrand } from '@deepkit/type-spec';
93-
import { Resolver } from './resolver.js';
93+
import { patternMatch, ReflectionMode, reflectionModeMatcher, reflectionModes, Resolver } from './resolver.js';
9494
import { knownLibFilesForCompilerOptions } from '@typescript/vfs';
95-
import * as micromatch from 'micromatch';
9695

9796
// don't use from @deepkit/core since we don't want to have a dependency to @deepkit/core
9897
export function isObject(obj: any): obj is { [key: string]: any } {
@@ -186,7 +185,6 @@ const serverEnv = 'undefined' !== typeof process;
186185
* It can't be more ops than this given number
187186
*/
188187
export const packSize: number = 2 ** packSizeByte; //64
189-
const reflectionModes = ['always', 'default', 'never'] as const;
190188

191189
interface ReflectionOptions {
192190
/**
@@ -695,7 +693,7 @@ export class ReflectionTransformer implements CustomTransformer {
695693
}
696694
}
697695

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

700698
if (this.reflectionMode === 'never') {
701699
return sourceFile;
@@ -992,15 +990,15 @@ export class ReflectionTransformer implements CustomTransformer {
992990
this.sourceFile = visitNode(this.sourceFile, compileDeclarations);
993991

994992
if (this.addImports.length) {
995-
const compilerOptions = this.compilerOptions;
996993
const handledIdentifier: string[] = [];
997994
for (const imp of this.addImports) {
998995
if (handledIdentifier.includes(getIdentifierName(imp.identifier))) continue;
999996
handledIdentifier.push(getIdentifierName(imp.identifier));
1000-
if (compilerOptions.module === ModuleKind.CommonJS) {
997+
if (this.getModuleType() === 'cjs') {
1001998
//var {identifier} = require('./bar')
999+
const test = this.f.createIdentifier(getIdentifierName(imp.identifier));
10021000
const variable = this.f.createVariableStatement(undefined, this.f.createVariableDeclarationList([this.f.createVariableDeclaration(
1003-
this.f.createObjectBindingPattern([this.f.createBindingElement(undefined, undefined, imp.identifier)]),
1001+
this.f.createObjectBindingPattern([this.f.createBindingElement(undefined, undefined, test)]),
10041002
undefined, undefined,
10051003
this.f.createCallExpression(this.f.createIdentifier('require'), undefined, [imp.from])
10061004
)], NodeFlags.Const));
@@ -1015,7 +1013,7 @@ export class ReflectionTransformer implements CustomTransformer {
10151013
//import {identifier} from './bar.js'
10161014
// import { identifier as identifier } is used to avoid automatic elision of imports (in angular builds for example)
10171015
// that's probably a bit unstable.
1018-
const specifier = this.f.createImportSpecifier(false, imp.identifier, imp.identifier);
1016+
const specifier = this.f.createImportSpecifier(false, undefined, imp.identifier);
10191017
const namedImports = this.f.createNamedImports([specifier]);
10201018
const importStatement = this.f.createImportDeclaration(undefined,
10211019
this.f.createImportClause(false, undefined, namedImports), imp.from
@@ -1111,6 +1109,10 @@ export class ReflectionTransformer implements CustomTransformer {
11111109
return this.sourceFile;
11121110
}
11131111

1112+
protected getModuleType(): 'cjs' | 'esm' {
1113+
return this.compilerOptions.module === ModuleKind.CommonJS ? 'cjs' : 'esm';
1114+
}
1115+
11141116
protected injectResetΩ<T extends FunctionDeclaration | FunctionExpression | MethodDeclaration | ConstructorDeclaration>(node: T): T {
11151117
let hasReceiveType = false;
11161118
for (const param of node.parameters) {
@@ -1997,12 +1999,7 @@ export class ReflectionTransformer implements CustomTransformer {
19971999

19982000
protected isExcluded(filePath: string): boolean {
19992001
if (!this.currentReflectionConfig.options.exclude) return false;
2000-
2001-
const excluded = micromatch.contains(filePath, this.currentReflectionConfig.options.exclude, {
2002-
basename: true,
2003-
cwd: this.currentReflectionConfig.baseDir
2004-
});
2005-
return excluded;
2002+
return patternMatch(filePath, this.currentReflectionConfig.options.exclude, this.currentReflectionConfig.baseDir);
20062003
}
20072004

20082005
protected extractPackStructOfTypeReference(type: TypeReferenceNode | ExpressionWithTypeArguments, program: CompilerProgram): void {
@@ -2144,12 +2141,15 @@ export class ReflectionTransformer implements CustomTransformer {
21442141
return;
21452142
}
21462143

2144+
const runtimeTypeName = this.getDeclarationVariableName(typeName);
2145+
21472146
//to break recursion, we track which declaration has already been compiled
21482147
if (!this.compiledDeclarations.has(declaration) && !this.compileDeclarations.has(declaration)) {
21492148
const declarationSourceFile = findSourceFile(declaration) || this.sourceFile;
21502149
const isGlobal = resolved.importDeclaration === undefined && declarationSourceFile.fileName !== this.sourceFile.fileName;
21512150
const isFromImport = resolved.importDeclaration !== undefined;
21522151

2152+
21532153
if (this.isExcluded(declarationSourceFile.fileName)) {
21542154
program.pushOp(ReflectionOp.any);
21552155
return;
@@ -2185,7 +2185,7 @@ export class ReflectionTransformer implements CustomTransformer {
21852185

21862186
// check if this is a viable option:
21872187
// //check if the referenced file has reflection info emitted. if not, any is emitted for that reference
2188-
// const typeVar = this.getDeclarationVariableName(typeName);
2188+
// const typeVar = runtimeRypeName;
21892189
// //check if typeVar is exported in referenced file
21902190
// const builtType = isNodeWithLocals(found) && found.locals && found.locals.has(typeVar.escapedText);
21912191
// if (!builtType) {
@@ -2201,7 +2201,7 @@ export class ReflectionTransformer implements CustomTransformer {
22012201
}
22022202

22032203
// this.addImports.push({ identifier: typeVar, from: resolved.importDeclaration.moduleSpecifier });
2204-
this.addImports.push({ identifier: this.getDeclarationVariableName(typeName), from: resolved.importDeclaration.moduleSpecifier });
2204+
this.addImports.push({ identifier: runtimeTypeName, from: resolved.importDeclaration.moduleSpecifier });
22052205
}
22062206
} else {
22072207
//it's a reference type inside the same file. Make sure its type is reflected
@@ -2219,7 +2219,7 @@ export class ReflectionTransformer implements CustomTransformer {
22192219
}
22202220

22212221
const index = program.pushStack(
2222-
program.forNode === declaration ? 0 : this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, this.getDeclarationVariableName(typeName))
2222+
program.forNode === declaration ? 0 : this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, runtimeTypeName)
22232223
);
22242224
if (type.typeArguments) {
22252225
for (const argument of type.typeArguments) {
@@ -2647,18 +2647,8 @@ export class ReflectionTransformer implements CustomTransformer {
26472647
);
26482648
}
26492649

2650-
protected parseReflectionMode(mode: typeof reflectionModes[number] | '' | boolean | string | string[] | undefined, configPathDir: string): typeof reflectionModes[number] {
2651-
if (Array.isArray(mode)) {
2652-
if (!configPathDir) return 'never';
2653-
const matches = micromatch.contains(this.sourceFile.fileName, mode, {
2654-
cwd: configPathDir
2655-
});
2656-
2657-
return matches ? 'default' : 'never';
2658-
}
2659-
if ('boolean' === typeof mode) return mode ? 'default' : 'never';
2660-
if (mode === 'default' || mode === 'always') return mode;
2661-
return 'never';
2650+
protected parseReflectionMode(mode: ReflectionMode, configPathDir: string): typeof reflectionModes[number] {
2651+
return reflectionModeMatcher(this.sourceFile.fileName, mode, configPathDir);
26622652
}
26632653

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

packages/type-compiler/src/plugin.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ts from 'typescript';
2+
import { cwd } from 'process';
3+
import { createFilter } from '@rollup/pluginutils';
4+
import { declarationTransformer, transformer } from './compiler.js';
5+
6+
export interface Options {
7+
include?: string;
8+
exclude?: string;
9+
tsConfig?: string;
10+
transformers?: ts.CustomTransformers;
11+
compilerOptions?: ts.CompilerOptions;
12+
readFile?: (path: string) => string | undefined;
13+
}
14+
15+
export type Transform = (code: string, fileName: string) => { code: string, map?: string } | undefined;
16+
17+
export function deepkitType(options: Options = {}): Transform {
18+
const filter = createFilter(options.include ?? ['**/*.tsx', '**/*.ts'], options.exclude ?? 'node_modules/**');
19+
20+
const transformers = options.transformers || {
21+
before: [transformer],
22+
after: [declarationTransformer],
23+
};
24+
25+
const configFilePath = options.tsConfig || cwd() + '/tsconfig.json';
26+
27+
const tsConfig = ts.readConfigFile(configFilePath, options.readFile || ts.sys.readFile);
28+
29+
if (tsConfig.error) {
30+
throw new Error(ts.formatDiagnostic(tsConfig.error, {
31+
getCanonicalFileName: (fileName: string) => fileName,
32+
getCurrentDirectory: ts.sys.getCurrentDirectory,
33+
getNewLine: () => ts.sys.newLine,
34+
}));
35+
}
36+
37+
const compilerOptions = Object.assign({
38+
'target': ts.ScriptTarget.ESNext,
39+
'module': ts.ModuleKind.ESNext,
40+
configFilePath,
41+
}, tsConfig.config, options.compilerOptions || {});
42+
43+
return function transform(code: string, fileName: string) {
44+
if (!filter(fileName)) return;
45+
46+
const transformed = ts.transpileModule(code, {
47+
compilerOptions,
48+
fileName,
49+
//@ts-ignore
50+
transformers
51+
});
52+
53+
return {
54+
code: transformed.outputText,
55+
map: transformed.sourceMapText,
56+
};
57+
};
58+
}

packages/type-compiler/src/resolver.ts

+35-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import type {
2-
CompilerHost,
3-
CompilerOptions,
4-
ExportDeclaration,
5-
Expression,
6-
ImportDeclaration,
7-
ResolvedModule,
8-
SourceFile,
9-
StringLiteral,
10-
} from 'typescript';
1+
import type { CompilerHost, CompilerOptions, ExportDeclaration, Expression, ImportDeclaration, ResolvedModule, SourceFile, StringLiteral, } from 'typescript';
112
import ts from 'typescript';
3+
import * as micromatch from 'micromatch';
4+
import { isAbsolute } from 'path';
125

136
const {
147
createSourceFile,
@@ -17,6 +10,38 @@ const {
1710
ScriptTarget,
1811
} = ts;
1912

13+
export const reflectionModes = ['always', 'default', 'never'] as const;
14+
15+
export type ReflectionMode = typeof reflectionModes[number] | '' | boolean | string | string[] | undefined;
16+
17+
export function patternMatch(path: string, patterns: string[], base?: string) {
18+
const normalized = patterns.map(v => {
19+
if (v[0] === '!') {
20+
if (!isAbsolute(v.slice(1))) return `!${base}/${v.substr(1)}`;
21+
return v;
22+
}
23+
24+
if (!isAbsolute(v)) return `${base}/${v}`;
25+
return v;
26+
})
27+
const matched = micromatch.default([path], normalized, {});
28+
return matched.length > 0;
29+
}
30+
31+
export function reflectionModeMatcher(
32+
filePath: string,
33+
mode: ReflectionMode,
34+
configPathDir?: string
35+
): typeof reflectionModes[number] {
36+
if (Array.isArray(mode)) {
37+
if (!configPathDir) return 'never';
38+
return patternMatch(filePath, mode, configPathDir) ? 'default' : 'never';
39+
}
40+
if ('boolean' === typeof mode) return mode ? 'default' : 'never';
41+
if (mode === 'default' || mode === 'always') return mode;
42+
return 'never';
43+
}
44+
2045
/**
2146
* A utility to resolve a module path and its declaration.
2247
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from '@jest/globals';
2+
import { patternMatch, ReflectionMode, reflectionModeMatcher } from '../src/resolver.js';
3+
4+
test('patternMatch', () => {
5+
expect(patternMatch('/app/model/test.ts', ['model/**/*.ts'], '/app')).toBe(true);
6+
expect(patternMatch('/app/model/test.ts', ['model/**/*.ts', '!**/*.ts'], '/app')).toBe(false);
7+
expect(patternMatch('/app/model/test.ts', ['model/**/*.ts', '!model/test.ts'], '/app')).toBe(false);
8+
});
9+
10+
test('match', () => {
11+
const mode: ReflectionMode = [
12+
'model/**/*.ts',
13+
];
14+
15+
expect(reflectionModeMatcher('/app/model/test.ts', mode, '/app')).toBe('default');
16+
expect(reflectionModeMatcher('/app/model/controller/test.controller.ts', mode, '/app')).toBe('default');
17+
expect(reflectionModeMatcher('/app/external/file.ts', mode, '/app')).toBe('never');
18+
});
19+
20+
test('match negate', () => {
21+
const mode: ReflectionMode = [
22+
'server/controllers/**/*.ts',
23+
'server/services/**/*.ts',
24+
'server/dao/**/*.ts',
25+
'!server/dao/mongoose.ts',
26+
'shared/**/*.ts',
27+
];
28+
29+
expect(reflectionModeMatcher('/path/portal/server/dao/models.ts', mode, '/path/portal')).toBe('default');
30+
expect(reflectionModeMatcher('/path/portal/server/dao/mongoose.ts', mode, '/path/portal')).toBe('never');
31+
});

packages/type-compiler/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"src",
2424
"tests",
2525
"install-transformer.ts",
26+
"compiler-debug.ts",
2627
"index.ts"
2728
],
2829
"references": [

0 commit comments

Comments
 (0)