Skip to content

Commit

Permalink
Merge pull request #124 from haxiomic/feature/hxnodejs
Browse files Browse the repository at this point in the history
Feature/hxnodejs
  • Loading branch information
haxiomic committed Jan 16, 2023
2 parents 830c036 + dc565ed commit 57e3daa
Show file tree
Hide file tree
Showing 17 changed files with 5,497 additions and 1,102 deletions.
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@
"args": ["maplibre-gl", "--verbose"],
"outputCapture": "std",
},
{
"type": "node",
"request": "launch",
"name": "dts2hx.js node ws",
"program": "${workspaceFolder}/dist/dts2hx.js",
"args": ["ws", "--verbose"],
"cwd": "${workspaceFolder}/test",
},
{
"type": "node",
"request": "launch",
Expand Down
38 changes: 36 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dts2hx",
"version": "0.19.2",
"version": "0.20.0",
"description": "Converts TypeScript definition files (d.ts) to haxe externs",
"keywords": [
"haxe",
Expand Down
19 changes: 17 additions & 2 deletions src/ConverterContext.hx
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,18 @@ class ConverterContext {
final anyUnionCollapse = false; // `any | string` -> `any`
final unionizedFunctionTypes = true; // `(?b) => C` -> `()->C | (b)->C`

final hxnodejsMap: Null<typemap.TypeMap>;

public function new(
inputModuleName: String,
moduleSearchPath: String,
compilerOptions: CompilerOptions,
stdLibMap: Null<typemap.TypeMap>,
hxnodejsMap: Null<typemap.TypeMap>,
options: Options
) {
this.options = options;
this.hxnodejsMap = hxnodejsMap;
// we make the moduleSearchPath absolute to work around an issue in resolveModuleName
moduleSearchPath = sys.FileSystem.absolutePath(moduleSearchPath);
this.moduleSearchPath = moduleSearchPath;
Expand Down Expand Up @@ -199,7 +203,12 @@ class ConverterContext {
program.assignModuleNames(moduleSearchPath, host);

// determine external dependencies:
moduleDependencies = program.getDependencies(inputModuleSourceFiles, normalizedInputModuleName, host);
var dependencies = program.getDependencies(inputModuleSourceFiles, normalizedInputModuleName, host);
// skip node types dependency if we are using hxnodejs
if (hxnodejsMap != null) {
dependencies = dependencies.filter(d -> d.normalizedModuleName != 'node');
}
moduleDependencies = dependencies;

// populate symbol access map
symbolAccessMap = new SymbolAccessMap(program);
Expand All @@ -210,7 +219,8 @@ class ConverterContext {
options.globalPackageName,
program,
symbolAccessMap,
stdLibMap
stdLibMap,
hxnodejsMap
);

// convert symbols, starting from entry-point file
Expand Down Expand Up @@ -328,6 +338,11 @@ class ConverterContext {
} else if (options.queueExternalSymbols) {
Log.log('Queuing external symbol', symbol);
declarationSymbolQueue.tryEnqueue(symbol);
} else if (hxnodejsMap != null) {
// when using hxnodejs we want to generate any node types that couldn't be matched
if (hxTypePath.pack.join('.').startsWith('js.node.')) {
declarationSymbolQueue.tryEnqueue(symbol);
}
}
}
}
Expand Down
143 changes: 112 additions & 31 deletions src/HaxeTypePathMap.hx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using tool.HaxeTools;
using tool.SymbolAccessTools;
using tool.TsProgramTools;
using tool.TsSymbolTools;
using StringTools;

/**
The idea here is to generate a haxe type-path for all symbols upfront (including external modules and build-in lib symbols).
Expand All @@ -26,19 +27,22 @@ class HaxeTypePathMap {
// the same symbol may have multiple type paths if it has multiple SymbolAccess
final symbolTypePathMap: Map<Int, Array<InternalModule>>;
final stdLibMap: Null<TypeMap>;
final hxnodejsMap: Null<TypeMap>;

public function new(
inputParentModuleName: String,
globalPackageName: Null<String>,
program: Program,
symbolAccessMap: SymbolAccessMap,
stdLibMap: Null<TypeMap>
stdLibMap: Null<TypeMap>,
hxnodejsMap: Null<TypeMap>
) {
this.inputParentModuleName = inputParentModuleName;
this.globalPackageName = globalPackageName;
this.program = program;
this.symbolAccessMap = symbolAccessMap;
this.stdLibMap = stdLibMap;
this.hxnodejsMap = hxnodejsMap;
this.tc = program.getTypeChecker();
symbolTypePathMap = buildHaxeTypePathMap();
}
Expand Down Expand Up @@ -90,7 +94,7 @@ class HaxeTypePathMap {
Log.warn('Internal error: unexpected symbol passed into `getTypePath()`', symbol);
}

debug();
// debug();

// we can generate a type-path on demand, but we won't have name deduplication, so it might be wrong
return generateTypePath(symbol, accessContext, preferInterfaceStructure);
Expand Down Expand Up @@ -320,6 +324,39 @@ class HaxeTypePathMap {
}
}

var name = getHaxeTypeNameFromAccess(symbol, access);
// prefix I if interface-structure version of a type
name = asInterfaceStructure ? 'I' + name : name;

// hxnodejs mapping
var isNodeJsType = symbol.getDeclarationsArray().exists(d -> d.getSourceFile().moduleName.startsWith('node/'));
if (hxnodejsMap != null) {
if (isNodeJsType) {
var tsTypeParamDeclarations = symbol.getDeclarationTypeParameters();
var matchedPath = matchHxnodejsType(symbol, access, name);
if (matchedPath != null) {
// check type-parameters match and generate externs for this built-in if they don't
var typeInfo = hxnodejsMap.typeInfo.get(matchedPath);
var typeParameterMatch = typeInfo != null && typeInfo.typeParameters.length == tsTypeParamDeclarations.length;
if (typeParameterMatch) {
var tp = HaxeTools.getTypePathFromString(matchedPath);
return {
name: tp.name,
moduleName: tp.moduleName,
pack: tp.pack,
isExistingStdLibType: true,
}
} else {
Log.warn('Type parameter mismatch between hxnodejs type <b>"$name"</> (<b>${typeInfo.typeParameters.length}</>) and node type (<b>${tsTypeParamDeclarations.length}</>)', symbol);
clashesWithBuiltIn = true; // rename to avoid clash with existing hxnodejs type
}
} else {
// no match found, generate as usual
Log.warn('node type <b>$name</> not found in hxnodejs externs', symbol);
}
}
}

function hasDeclarationInLib(symbol: Symbol, filename: String) {
for (declaration in symbol.getDeclarationsArray()) {
var sourceFile = declaration.getSourceFile();
Expand All @@ -330,11 +367,14 @@ class HaxeTypePathMap {
return false;
}

// if the symbol derives from a built-in, prefix js.lib or js.html
// otherwise prefix the module name (if it's a path, add a pack for each directory)
var pack = if (isBuiltIn) {
// if the symbol derives from a built-in, prefix js.lib or js.html
// otherwise prefix the module name (if it's a path, add a pack for each directory)
if (hasDeclarationInLib(symbol, 'lib.dom.d.ts')) ['js', 'html']
else ['js', 'lib'];
} else if (isNodeJsType) {
// if the symbol derives from a node.js type, prefix `js` (default is just `node`)
['js'];
} else {
[];
}
Expand Down Expand Up @@ -378,23 +418,41 @@ class HaxeTypePathMap {
// make pack a safe package path
pack = pack.map(s -> s.toSafePackageName());

/**
When generating a haxe module name for a symbol, rather than using the symbol's name, we use the name used to _access_ the symbol
This may be different from the symbol name itself. For example, say we have a module which uses `export =`
```typescript
declare module "lib/fs" {
// rename if name that conflict with std.* types
// @! we should generate this list automatically in the future
var disallowedNames = stdLibMap != null ? stdLibMap.topLevelNames : defaultDisallowedNames;
// add '_' to avoid disallowed names
if (disallowedNames.indexOf(name) != -1 || clashesWithBuiltIn) {
name = name + '_';
}

class internal {
// ...
}
// handle short aliases
return {
moduleName: name,
name: name,
pack: pack,
isExistingStdLibType: false,
}
}

export = internal;
/**
When generating a haxe module name for a symbol, rather than using the symbol's name, we use the name used to _access_ the symbol
This may be different from the symbol name itself. For example, say we have a module which uses `export =`
```typescript
declare module "lib/fs" {
class internal {
// ...
}
```
References to the class `internal` should be exposed as references to `"lib/fs"`, and `internal` should not appear in the generated haxe
**/
export = internal;
}
```
References to the class `internal` should be exposed as references to `"lib/fs"`, and `internal` should not appear in the generated haxe
**/
function getHaxeTypeNameFromAccess(symbol: Symbol, access: SymbolAccess) {
var typeIdentifier: String = switch access {
case AmbientModule(path, _, symbolChain), ExportModule(path, _, symbolChain):
var lastSymbol = symbolChain[symbolChain.length - 1];
Expand All @@ -421,25 +479,48 @@ class HaxeTypePathMap {
case Inaccessible:
symbol.name;
}
var name = typeIdentifier.toSafeTypeName();
// prefix I if interface-structure version of a type
name = asInterfaceStructure ? 'I' + name : name;
return typeIdentifier.toSafeTypeName();
}

// rename if name that conflict with std.* types
// @! we should generate this list automatically in the future
var disallowedNames = stdLibMap != null ? stdLibMap.topLevelNames : defaultDisallowedNames;
// add '_' to avoid disallowed names
if (disallowedNames.indexOf(name) != -1 || clashesWithBuiltIn) {
name = name + '_';
function matchHxnodejsType(symbol: Symbol, access: SymbolAccess, name: String): Null<String> {
var accessMeta = access.toAccessMetadata();
if (accessMeta.name == ':jsRequire') {
// we can use the jsRequire lookup to find the hxnodejs type
var jsRequirePath = accessMeta.params.map(p -> haxe.macro.ExprTools.getValue(p)).join('/');
var jsRequireHaxePath = hxnodejsMap.jsRequireMap.get(jsRequirePath);
if (jsRequireHaxePath != null) {
return jsRequireHaxePath;
}
}

// handle short aliases
return {
moduleName: name,
name: name,
pack: pack,
isExistingStdLibType: false,
// try searching the lowercaseLookup
var lookupEntries = hxnodejsMap.lowercaseLookup.get(name.toLowerCase());
if (lookupEntries != null) {
// find closest match: check if node package matches node.js module name

var closestMatches = switch access {
case AmbientModule(nodeModule, _, _), ExportModule(nodeModule, _, _):
nodeModule = tool.StringTools.unwrapQuotes(nodeModule);
// find js.node.$m
lookupEntries.filter(p -> p.toLowerCase().startsWith('js.node.${nodeModule.toLowerCase()}.'));

case Global(_), Inaccessible:
// js.node level
lookupEntries.filter(p -> {
var tp = HaxeTools.getTypePathFromString(p);
return tp.pack.length == 2 && tp.pack[0] == 'js' && tp.pack[1] == 'node';
});
}

if (closestMatches.length > 0) {
return closestMatches[0];
} else if (lookupEntries.length == 1) {
Log.warn('Could not find an exact match for hxnodejs type "${name}" (using ${lookupEntries[0]})');
return lookupEntries[0];
}
}

return null;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Log.hx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Log {
}

static public function symbolInfo(symbol: Symbol): String {
if (symbol == null) return '<red>null</> (symbol is null)';
var str = '<b,cyan>${symbol.name} ${TsSymbolTools.getFlagsInfo(symbol)}</>';
if (symbol.valueDeclaration != null) {
str += ' ' + nodeInfo(symbol.valueDeclaration);
Expand Down
Loading

0 comments on commit 57e3daa

Please sign in to comment.