Skip to content

Commit 4d3aafe

Browse files
authored
feat(ts-interface-generator): support ts path mappings with configDir template variable (#492)
fix #484
1 parent 81150ba commit 4d3aafe

File tree

9 files changed

+249
-18
lines changed

9 files changed

+249
-18
lines changed

packages/ts-interface-generator/src/addSourceExports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function addSourceExports(
3636
const filePath = path.normalize(fileName); // e.g. 'c:\SAPDevelop\git\ui5-typescript-control-library\src-ts\com\myorg\myUI5Library\Example.ts
3737
let globalName: string, moduleFileName: string;
3838
for (let i = 0; i < allPathMappings.length; i++) {
39-
const fullTargetPath = path.join(basePath, allPathMappings[i].target);
39+
const fullTargetPath = path.resolve(basePath, allPathMappings[i].target);
4040
if (filePath.indexOf(fullTargetPath) === 0) {
4141
const restPath = filePath.replace(fullTargetPath, "");
4242
moduleFileName = path

packages/ts-interface-generator/src/generateTSInterfacesAPI.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,14 @@ export function main(args: Args) {
6060
}
6161

6262
/**
63-
* Whenever the code changes, this is called, in oder to re-generate the interfaces
64-
*
63+
* Extracts known local exports and relevant source files from program.
6564
*
66-
* TODO: can we use the knowledge about the changed files to limit the scope here?
67-
* A Problem is that also a change in a different file could influence the generation of an unchanged file. E.g. when an update of the
68-
* UI5 type definitions changes the base class of sap.m.Button to something where API methods are not generated, then
69-
* the next generation run would no longer create a *.gen.d.ts file for Controls deriving from Button (ok, extreme example...)
7065
* @param program
7166
* @param typeChecker
72-
* @param changedFiles
73-
* @param allKnownGlobals
7467
*/
75-
function onTSProgramUpdate(
68+
export function getProgramInfo(
7669
program: ts.Program,
7770
typeChecker: ts.TypeChecker,
78-
changedFiles: string[], // is an empty array in non-watch case; is at least one file in watch case - but overall not reliable!
79-
allKnownGlobals: GlobalToModuleMapping,
8071
) {
8172
// this block collects all path mappings from the compiler configuration, so we can find out the logical name for a concrete file path
8273
const paths = program.getCompilerOptions().paths;
@@ -134,6 +125,33 @@ function onTSProgramUpdate(
134125
); // extract all local exports
135126
});
136127

128+
return { allRelevantSourceFiles, allKnownLocalExports };
129+
}
130+
131+
/**
132+
* Whenever the code changes, this is called, in oder to re-generate the interfaces
133+
*
134+
*
135+
* TODO: can we use the knowledge about the changed files to limit the scope here?
136+
* A Problem is that also a change in a different file could influence the generation of an unchanged file. E.g. when an update of the
137+
* UI5 type definitions changes the base class of sap.m.Button to something where API methods are not generated, then
138+
* the next generation run would no longer create a *.gen.d.ts file for Controls deriving from Button (ok, extreme example...)
139+
* @param program
140+
* @param typeChecker
141+
* @param changedFiles
142+
* @param allKnownGlobals
143+
*/
144+
function onTSProgramUpdate(
145+
program: ts.Program,
146+
typeChecker: ts.TypeChecker,
147+
changedFiles: string[], // is an empty array in non-watch case; is at least one file in watch case - but overall not reliable!
148+
allKnownGlobals: GlobalToModuleMapping,
149+
) {
150+
const { allRelevantSourceFiles, allKnownLocalExports } = getProgramInfo(
151+
program,
152+
typeChecker,
153+
);
154+
137155
// now actually generate the interface files for all source files
138156
allRelevantSourceFiles.forEach((sourceFile) => {
139157
generateInterfaces(

packages/ts-interface-generator/src/test/testcases/testcaseRunner.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ts from "typescript";
44
import log from "loglevel";
55
import { generateInterfaces } from "../../interfaceGenerationHelper";
66
import { getAllKnownGlobals, GlobalToModuleMapping } from "../../typeScriptEnvironment";
7+
import { getProgramInfo } from "../../generateTSInterfacesAPI";
78

89
const testCasesDir = path.resolve(__dirname);
910

@@ -34,6 +35,18 @@ describe("Single Testcases", () => {
3435
return;
3536
}
3637

38+
function parseConfig(tsConfigPath: string) {
39+
let config: ts.CompilerOptions;
40+
if (fs.existsSync(tsConfigPath)) {
41+
const json: unknown = ts.parseConfigFileTextToJson(tsConfigPath, fs.readFileSync(tsConfigPath).toString()).config || {};
42+
config = ts.parseJsonConfigFileContent(json, ts.sys, path.dirname(tsConfigPath)).options;
43+
} else {
44+
config = {...standardTsConfig};
45+
}
46+
config.baseUrl = testCaseDir;
47+
return config;
48+
}
49+
3750
test(`Interface generation for ${testCase}`, async () => {
3851
// setup TypeScript program
3952
const tsConfigPath = path.join(testCaseDir, "tsconfig.json");
@@ -42,15 +55,13 @@ describe("Single Testcases", () => {
4255
.filter((file) => file.endsWith(".ts") && !file.endsWith(".d.ts"))
4356
.map((file) => path.join(testCaseDir, file));
4457

45-
const tsConfig = fs.existsSync(tsConfigPath)
46-
? { configFilePath: tsConfigPath }
47-
: standardTsConfig;
48-
const program = ts.createProgram(tsFiles, tsConfig);
58+
const program = ts.createProgram(tsFiles, parseConfig(tsConfigPath));
4959
const typeChecker = program.getTypeChecker();
60+
const programInfo = getProgramInfo(program, typeChecker);
5061
const allKnownGlobals: GlobalToModuleMapping = getAllKnownGlobals(typeChecker);
5162

5263
const sourceFiles = program.getSourceFiles().filter((sourceFile) => {
53-
return !sourceFile.isDeclarationFile;
64+
return !sourceFile.isDeclarationFile && path.basename(sourceFile.fileName) !== "library.ts";
5465
});
5566

5667
const runGenerateInterfaces = async (
@@ -68,7 +79,7 @@ describe("Single Testcases", () => {
6879
generateInterfaces(
6980
sourceFile,
7081
typeChecker,
71-
allKnownGlobals,
82+
Object.assign({}, programInfo.allKnownLocalExports, allKnownGlobals),
7283
resultProcessor,
7384
);
7485
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { MyJSEnum } from "my/library";
2+
import { MyTSEnum } from "my/library";
3+
import MyDependency from "my/MyDependency";
4+
import { PropertyBindingInfo } from "sap/ui/base/ManagedObject";
5+
import { $ControlSettings } from "sap/ui/core/Control";
6+
7+
declare module "./MyControl" {
8+
9+
/**
10+
* Interface defining the settings object used in constructor calls
11+
*/
12+
interface $MyControlSettings extends $ControlSettings {
13+
myJSEnumVal?: MyJSEnum | PropertyBindingInfo | `{${string}}`;
14+
myTSEnumVal?: MyTSEnum | PropertyBindingInfo | `{${string}}`;
15+
myDependency?: MyDependency | PropertyBindingInfo | `{${string}}`;
16+
}
17+
18+
export default interface MyControl {
19+
20+
// property: myJSEnumVal
21+
22+
/**
23+
* Gets current value of property "myJSEnumVal".
24+
*
25+
* Default value is: "MyJSEnum.Foo,"
26+
* @returns Value of property "myJSEnumVal"
27+
*/
28+
getMyJSEnumVal(): MyJSEnum;
29+
30+
/**
31+
* Sets a new value for property "myJSEnumVal".
32+
*
33+
* When called with a value of "null" or "undefined", the default value of the property will be restored.
34+
*
35+
* Default value is: "MyJSEnum.Foo,"
36+
* @param [myJSEnumVal="MyJSEnum.Foo,"] New value for property "myJSEnumVal"
37+
* @returns Reference to "this" in order to allow method chaining
38+
*/
39+
setMyJSEnumVal(myJSEnumVal: MyJSEnum): this;
40+
41+
// property: myTSEnumVal
42+
43+
/**
44+
* Gets current value of property "myTSEnumVal".
45+
*
46+
* Default value is: "MyTSEnum.Foo,"
47+
* @returns Value of property "myTSEnumVal"
48+
*/
49+
getMyTSEnumVal(): MyTSEnum;
50+
51+
/**
52+
* Sets a new value for property "myTSEnumVal".
53+
*
54+
* When called with a value of "null" or "undefined", the default value of the property will be restored.
55+
*
56+
* Default value is: "MyTSEnum.Foo,"
57+
* @param [myTSEnumVal="MyTSEnum.Foo,"] New value for property "myTSEnumVal"
58+
* @returns Reference to "this" in order to allow method chaining
59+
*/
60+
setMyTSEnumVal(myTSEnumVal: MyTSEnum): this;
61+
62+
// property: myDependency
63+
64+
/**
65+
* Gets current value of property "myDependency".
66+
*
67+
* @returns Value of property "myDependency"
68+
*/
69+
getMyDependency(): MyDependency;
70+
71+
/**
72+
* Sets a new value for property "myDependency".
73+
*
74+
* When called with a value of "null" or "undefined", the default value of the property will be restored.
75+
*
76+
* @param myDependency New value for property "myDependency"
77+
* @returns Reference to "this" in order to allow method chaining
78+
*/
79+
setMyDependency(myDependency: MyDependency): this;
80+
}
81+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Control from "sap/ui/core/Control";
2+
import type { MetadataOptions } from "sap/ui/core/Element";
3+
import { MyJSEnum, MyTSEnum } from "./library";
4+
5+
export default class MyControl extends Control {
6+
static readonly metadata: MetadataOptions = {
7+
properties: {
8+
myJSEnumVal: {
9+
type: "my.MyJSEnum",
10+
defaultValue: MyJSEnum.Foo,
11+
},
12+
myTSEnumVal: {
13+
type: "my.MyTSEnum",
14+
defaultValue: MyTSEnum.Foo,
15+
},
16+
myDependency: {
17+
type: "my.MyDependency",
18+
},
19+
},
20+
};
21+
constructor(idOrSettings?: string | $MyControlSettings);
22+
constructor(id?: string, settings?: $MyControlSettings);
23+
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { PropertyBindingInfo } from "sap/ui/base/ManagedObject";
2+
import { $ControlSettings } from "sap/ui/core/Control";
3+
4+
declare module "./MyDependency" {
5+
6+
/**
7+
* Interface defining the settings object used in constructor calls
8+
*/
9+
interface $MyDependencySettings extends $ControlSettings {
10+
foo?: string | PropertyBindingInfo;
11+
}
12+
13+
export default interface MyDependency {
14+
15+
// property: foo
16+
17+
/**
18+
* Gets current value of property "foo".
19+
*
20+
* Default value is: "bar"
21+
* @returns Value of property "foo"
22+
*/
23+
getFoo(): string;
24+
25+
/**
26+
* Sets a new value for property "foo".
27+
*
28+
* When called with a value of "null" or "undefined", the default value of the property will be restored.
29+
*
30+
* Default value is: "bar"
31+
* @param [foo="bar"] New value for property "foo"
32+
* @returns Reference to "this" in order to allow method chaining
33+
*/
34+
setFoo(foo: string): this;
35+
}
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Control from "sap/ui/core/Control";
2+
import type { MetadataOptions } from "sap/ui/core/Element";
3+
4+
export default class MyDependency extends Control {
5+
static readonly metadata: MetadataOptions = {
6+
properties: {
7+
foo: {
8+
type: "string",
9+
defaultValue: "bar",
10+
},
11+
},
12+
};
13+
14+
constructor(idOrSettings?: string | $MyDependencySettings);
15+
constructor(id?: string, settings?: $MyDependencySettings);
16+
constructor(id?: string, settings?: $MyDependencySettings) { super(id, settings); }
17+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import DataType from "sap/ui/base/DataType";
2+
import Lib from "sap/ui/core/Lib";
3+
4+
const thisLib: object&{MyJSEnum?: object; MyTSEnum?: object} = Lib.init({
5+
name: "my",
6+
version: "${version}",
7+
dependencies: ["sap.ui.core"],
8+
types: [],
9+
interfaces: [],
10+
controls: [],
11+
elements: [],
12+
noLibraryCSS: false,
13+
});
14+
15+
export const MyJSEnum = {
16+
Foo: "foo",
17+
Bar: "bar",
18+
} as const;
19+
thisLib.MyJSEnum = MyJSEnum;
20+
DataType.registerEnum("mylib.MyJSEnum", thisLib.MyJSEnum);
21+
22+
export enum MyTSEnum {
23+
Foo = "foo",
24+
Bar = "bar",
25+
}
26+
thisLib.MyTSEnum = MyTSEnum;
27+
DataType.registerEnum("mylib.MyTSEnum", thisLib.MyTSEnum);
28+
29+
export default thisLib;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"strict": true,
6+
"moduleResolution": "Node16",
7+
"esModuleInterop": true,
8+
"skipLibCheck": true,
9+
"forceConsistentCasingInFileNames": true,
10+
"noEmit": true,
11+
"paths": {
12+
"my/*": ["${configDir}/*"]
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)