diff --git a/.eslintrc.js b/.eslintrc.js index c40772c7..f2dec7ef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,6 @@ module.exports = { "**/godot_src/**", "**/js/**", ".eslintrc.js", - "tsconfig.json", ], env: { es2021: true, diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..85012033 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto + +# Declare files that will always have LF line endings on checkout. +*.ts text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3126eb20..f6183c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +example/_godot_defs/dynamic +example/**/*.gd node_modules/ js/ .DS_Store @@ -5,4 +7,4 @@ js/ # Godot-specific ignores .import/ export.cfg -export_presets.cfg \ No newline at end of file +export_presets.cfg diff --git a/.vscode/launch.json b/.vscode/launch.json index d29f6ee0..07ba40b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,14 +5,38 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch Program", + "name": "Run mockProject", "program": "${workspaceFolder}/js/main.js", "request": "launch", - "skipFiles": ["/**"], - "args": ["${workspaceFolder}/example/ts2gd.json"], + "skipFiles": [ + "/**" + ], + "args": [ + "${workspaceFolder}/mockProject/ts2gd.json", + "--debug" + ], "type": "pwa-node", "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": ["${workspaceFolder}/js/**/*.js"] + "outFiles": [ + "${workspaceFolder}/js/**/*.js" + ] + }, + { + "name": "Run Tests", + "program": "${workspaceFolder}/js/tests/test.js", + "args": [ + "${workspaceFolder}/example/ts2gd.json", + "--debug" + ], + "request": "launch", + "skipFiles": [ + "/**" + ], + "type": "pwa-node", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/js/**/*.js" + ] } ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b22407e4..2e3913e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "typescript.tsserver.experimental.enableProjectDiagnostics": true, // "typescript.tsserver.experimental.enableProjectDiagnostics": true // "typescript.preferences.importModuleSpecifierEnding": "js" - // "typescript.tsserver.log": "verbose" + // "typescript.tsserver.log": "verbose", + "files.eol": "\n" } \ No newline at end of file diff --git a/README.md b/README.md index 8116f462..0512a6e6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,16 @@ To compile all source files once: `ts2gd --buildOnly` +## Windows + +ts2gd should run on GNU/Linux, Mac OS and Windows. If you run into issues, please fill a ticket. + +Development of ts2gd makes use of symlinks within the repository, thus when cloning on windows, clone with: + +``` +git clone -c core.symlinks=true --recurse-submodules git@github.com:johnfn/ts2gd.git +``` + ## Details and Differences ### `get_node` diff --git a/errors.ts b/errors.ts deleted file mode 100644 index 2564544e..00000000 --- a/errors.ts +++ /dev/null @@ -1,112 +0,0 @@ -import chalk from "chalk" -import ts from "typescript" - -import { ParsedArgs } from "./parse_args" - -export enum ErrorName { - InvalidNumber, - InvalidImport, - ClassNameNotFound, - ClassDoesntExtendAnything, - ClassMustBeExported, - TooManyClassesFound, - ClassCannotBeAnonymous, - TwoClassesWithSameName, - CantFindAutoloadInstance, - UnknownTsSyntax, - PathNotFound, - ExportedVariableError, - InvalidFile, - - Ts2GdError, - - AutoloadProjectButNotDecorated, - AutoloadDecoratedButNotProject, - AutoloadNotExported, - - NoComplicatedConnect, - - SignalsMustBePrefixedWith$, - - DeclarationNotGiven, -} - -// export type TsGdReturn = { -// errors?: TsGdError[] -// result: T -// } - -export type TsGdError = { - error: ErrorName - location: ts.Node | string - stack: string - description: string -} - -let errors: TsGdError[] = [] - -export const addError = (error: TsGdError) => { - errors.push(error) -} - -export const displayErrors = (args: ParsedArgs, message: string) => { - if (!args.debug) { - console.clear() - } - - if (errors.length === 0) { - console.info(message) - console.info() - console.info(chalk.greenBright("No errors.")) - - return false - } - - console.info(message) - console.info() - console.info( - chalk.redBright(`${errors.length} error${errors.length > 1 ? "s" : ""}.`) - ) - - for (const error of errors) { - if (typeof error.location === "string") { - console.warn(`${chalk.blueBright(error.location)}`) - } else { - const lineAndChar = error.location - .getSourceFile() - ?.getLineAndCharacterOfPosition(error.location.getStart()) - - if (!lineAndChar && args.debug) { - console.log(lineAndChar) - console.log(error.location) - console.log(error.stack) - } - - const { line, character } = lineAndChar - - console.warn() - console.warn( - `${chalk.blueBright( - error.location.getSourceFile().fileName - )}:${chalk.yellow(line + 1)}:${chalk.yellow(character + 1)}` - ) - - if (args.debug) { - console.log(error.stack) - } - } - - console.info(error.description) - } - - errors = [] - return true -} - -export const __getErrorsTestOnly = () => { - const result = errors - - errors = [] - - return result -} diff --git a/generate_library_defs/library_builder.ts b/generate_library_defs/library_builder.ts index 918981d6..3661fc04 100644 --- a/generate_library_defs/library_builder.ts +++ b/generate_library_defs/library_builder.ts @@ -357,7 +357,7 @@ declare var ${className}: typeof ${className}Constructor & { fs.writeFileSync( path.join( this.paths.staticGodotDefsPath, - fileName.slice(0, -4) + ".d.ts" + this.paths.replaceExtension(fileName, ".d.ts") ), result ) diff --git a/main.ts b/main.ts index f43ae20d..7ac3775b 100755 --- a/main.ts +++ b/main.ts @@ -239,7 +239,7 @@ const setup = (tsgdJson: Paths) => { opt.config.useCaseSensitiveFileNames = false return { - watchProgram, + program: watchProgram.getProgram().getProgram(), tsgdJson, reportWatchStatusChanged, tsInitializationFinished, @@ -265,10 +265,10 @@ export const main = async (args: ParsedArgs) => { const tsgdJson = new Paths(args) showLoadingMessage("Initializing TypeScript", args) - const { watchProgram, tsInitializationFinished } = setup(tsgdJson) + const { program, tsInitializationFinished } = setup(tsgdJson) showLoadingMessage("Scanning project", args) - let project = await makeTsGdProject(tsgdJson, watchProgram, args) + let project = await makeTsGdProject(tsgdJson, program, args) if (args.buildLibraries || project.shouldBuildLibraryDefinitions(args)) { showLoadingMessage("Building definition files", args) diff --git a/mockProject/Autoload.ts b/mockProject/Autoload.ts new file mode 100644 index 00000000..ccc77d84 --- /dev/null +++ b/mockProject/Autoload.ts @@ -0,0 +1,6 @@ +// this file will have dynamic content from our test infrastructure + +@autoload +class Autoload extends Node2D { + public hello = "hi" +} diff --git a/mockProject/Enum.ts b/mockProject/Enum.ts new file mode 100644 index 00000000..8ea6c20c --- /dev/null +++ b/mockProject/Enum.ts @@ -0,0 +1,8 @@ +enum SomeEnum { + Value1, + Value2, +} + +export class EnumUser { + callSomething(input: SomeEnum): void {} +} diff --git a/mockProject/Main.ts b/mockProject/Main.ts new file mode 100644 index 00000000..76a41512 --- /dev/null +++ b/mockProject/Main.ts @@ -0,0 +1 @@ +export class Main {} diff --git a/mockProject/Main.tscn b/mockProject/Main.tscn index b97362bb..a5627ba9 100644 --- a/mockProject/Main.tscn +++ b/mockProject/Main.tscn @@ -1,6 +1,10 @@ -[gd_scene format=2] +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://Main.gd" type="Script" id=1] +[ext_resource path="res://nested/Nested.tscn" type="PackedScene" id=2] [node name="Node2D" type="Node2D"] +script = ExtResource( 1 ) [node name="Hello" type="Label" parent="."] margin_right = 1027.0 @@ -11,3 +15,5 @@ valign = 1 __meta__ = { "_edit_use_anchors_": false } + +[node name="Nested" parent="." instance=ExtResource( 2 )] diff --git a/mockProject/Test.ts b/mockProject/Test.ts new file mode 100644 index 00000000..0d2b0efd --- /dev/null +++ b/mockProject/Test.ts @@ -0,0 +1,2 @@ +// this file will have dynamic content from our test infrastructure +export class Test {} diff --git a/mockProject/main.tscn b/mockProject/main.tscn new file mode 100644 index 00000000..a5627ba9 --- /dev/null +++ b/mockProject/main.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://Main.gd" type="Script" id=1] +[ext_resource path="res://nested/Nested.tscn" type="PackedScene" id=2] + +[node name="Node2D" type="Node2D"] +script = ExtResource( 1 ) + +[node name="Hello" type="Label" parent="."] +margin_right = 1027.0 +margin_bottom = 598.0 +text = "Welcome to TS2GD" +align = 1 +valign = 1 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Nested" parent="." instance=ExtResource( 2 )] diff --git a/mockProject/nested/Nested.ts b/mockProject/nested/Nested.ts new file mode 100644 index 00000000..9ffe168f --- /dev/null +++ b/mockProject/nested/Nested.ts @@ -0,0 +1,3 @@ +export class Nested { + constructor() {} +} diff --git a/mockProject/nested/Nested.tscn b/mockProject/nested/Nested.tscn new file mode 100644 index 00000000..3e5bcd60 --- /dev/null +++ b/mockProject/nested/Nested.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://nested/Nested.gd" type="Script" id=1] + +[node name="Nested" type="Node2D"] +script = ExtResource( 1 ) diff --git a/mockProject/project.godot b/mockProject/project.godot index 2245317f..db5b26d7 100644 --- a/mockProject/project.godot +++ b/mockProject/project.godot @@ -8,12 +8,44 @@ config_version=4 +_global_script_classes=[ { +"base": "Reference", +"class": "Test", +"language": "GDScript", +"path": "res://Test.gd" +}, { +"class": "Main", +"language": "GDScript", +"path": "res://Main.gd" +}, { +"base": "Reference", +"class": "Nested", +"language": "GDScript", +"path": "res://nested/Nested.gd" +} ] + +_global_script_class_icons={ +"Test": "", +"Main": "", +"Nested": "" +} + [application] config/name="mockProject" -run/main_scene="res://Main.tscn" +run/main_scene="res://main.tscn" config/icon="res://icon.png" +[autoload] + +Autoload="*res://Autoload.gd" + +[global] + +autoload=false +tabs=false +spaces=false + [physics] common/enable_pause_aware_picking=true diff --git a/mockProject/ts2gd.json b/mockProject/ts2gd.json index fb9fc38c..ca3b6e65 100644 --- a/mockProject/ts2gd.json +++ b/mockProject/ts2gd.json @@ -1,6 +1,6 @@ { "destination": "./", "source": "./", - "godotSourceRepoPath": "./godot_src", + "godotSourceRepoPath": "../godot_src", "ignore": ["**/ignore_me/**", "ignore_me.ts"] } diff --git a/package-lock.json b/package-lock.json index c5738af9..f5e47071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.2", + "lodash": "^4.17.21", "pkginfo": "^0.4.1", "tsutils": "^3.21.0", "xml2js": "^0.4.23" @@ -19,6 +20,7 @@ "ts2gd": "bin/index.js" }, "devDependencies": { + "@types/lodash": "^4.14.178", "@types/node": "^16.11.9", "@types/pkginfo": "^0.4.0", "@types/xml2js": "^0.4.9", @@ -180,6 +182,12 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, "node_modules/@types/node": { "version": "16.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", @@ -2167,6 +2175,11 @@ "node": ">=4" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3562,6 +3575,12 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lodash": { + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "dev": true + }, "@types/node": { "version": "16.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", @@ -4988,6 +5007,11 @@ "path-exists": "^3.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 713417a8..0bdad81b 100644 --- a/package.json +++ b/package.json @@ -33,19 +33,24 @@ "semi": false }, "lint-staged": { - "*.{ts,json}": [ + "*.ts": [ "eslint --fix", "prettier --write" + ], + "*.json": [ + "prettier --write" ] }, "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.2", + "lodash": "^4.17.21", "pkginfo": "^0.4.1", "tsutils": "^3.21.0", "xml2js": "^0.4.23" }, "devDependencies": { + "@types/lodash": "^4.14.178", "@types/node": "^16.11.9", "@types/pkginfo": "^0.4.0", "@types/xml2js": "^0.4.9", diff --git a/parse_node.ts b/parse_node.ts index f9b34c2a..8bd21d03 100644 --- a/parse_node.ts +++ b/parse_node.ts @@ -2,7 +2,7 @@ import * as utils from "tsutils" import ts, { SyntaxKind, tokenToString } from "typescript" import { AssetSourceFile } from "./project/assets/asset_source_file" -import { ErrorName, TsGdError, addError } from "./errors" +import TsGdProject, { ErrorName } from "./project" import { LibraryFunctionName } from "./parse_node/library_functions" import { Scope } from "./scope" import { @@ -63,7 +63,6 @@ import { parseVariableDeclarationList } from "./parse_node/parse_variable_declar import { parseVariableStatement } from "./parse_node/parse_variable_statement" import { parseWhileStatement } from "./parse_node/parse_while_statement" import { parseYieldExpression } from "./parse_node/parse_yield_expression" -import TsGdProject from "./project" export type ParseState = { isConstructor: boolean @@ -483,7 +482,7 @@ export const parseNode = ( default: console.error("Unknown token:", syntaxKindToString(genericNode.kind)) - addError({ + props.project.errors.add({ error: ErrorName.UnknownTsSyntax, location: genericNode, stack: new Error().stack ?? "", diff --git a/parse_node/parse_arrow_function.ts b/parse_node/parse_arrow_function.ts index 4cdf4760..73ed478c 100644 --- a/parse_node/parse_arrow_function.ts +++ b/parse_node/parse_arrow_function.ts @@ -1,6 +1,6 @@ import ts, { SyntaxKind } from "typescript" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project/errors" import { ParseNodeType, ParseState, combine } from "../parse_node" /** @@ -38,7 +38,7 @@ const getFreeVariables = ( if (symbol) { if (!symbol.declarations) { - addError({ + props.project.errors.add({ error: ErrorName.DeclarationNotGiven, location: node, stack: new Error().stack ?? "", @@ -183,7 +183,7 @@ ${unwrapCapturedScope} .symbol?.declarations if (!decls) { - addError({ + props.project.errors.add({ error: ErrorName.DeclarationNotGiven, location: node, stack: new Error().stack ?? "", diff --git a/parse_node/parse_call_expression.ts b/parse_node/parse_call_expression.ts index f2e656c3..98bd8b7e 100644 --- a/parse_node/parse_call_expression.ts +++ b/parse_node/parse_call_expression.ts @@ -1,6 +1,6 @@ import ts, { SyntaxKind } from "typescript" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project/errors" import { ExtraLine, ExtraLineType, @@ -202,7 +202,7 @@ export const parseCallExpression = ( } if (!parsedArgs[0]) { - addError({ + props.project.errors.add({ description: "Missing arrow function argument in signal connect invocation.", error: ErrorName.Ts2GdError, @@ -217,7 +217,7 @@ export const parseCallExpression = ( ) if (!arrowFunctionObj) { - addError({ + props.project.errors.add({ description: "ts2gd can't find that arrow function. This is an internal ts2gd error. Please report it on GitHub along with the code that caused it.", error: ErrorName.Ts2GdError, @@ -294,7 +294,7 @@ export const parseCallExpression = ( content: expressionWithoutRpcName, } } else { - addError({ + props.project.errors.add({ description: "I'm confused by this rpc", error: ErrorName.Ts2GdError, location: pae.expression, diff --git a/parse_node/parse_class_declaration.ts b/parse_node/parse_class_declaration.ts index e1da52da..7d179f52 100644 --- a/parse_node/parse_class_declaration.ts +++ b/parse_node/parse_class_declaration.ts @@ -1,6 +1,6 @@ import ts, { SyntaxKind } from "typescript" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project/errors" import { ParseNodeType, ParseState, combine } from "../parse_node" import { Test } from "../tests/test" import { getGodotType } from "../ts_utils" @@ -91,7 +91,7 @@ export const parseClassDeclaration = ( ) if (!modifiers?.includes("export") && !isAutoload) { - addError({ + props.project.errors.add({ description: "You must export this class.", error: ErrorName.ClassMustBeExported, location: node, diff --git a/parse_node/parse_enum_declaration.ts b/parse_node/parse_enum_declaration.ts index ac5b62d7..1474014e 100644 --- a/parse_node/parse_enum_declaration.ts +++ b/parse_node/parse_enum_declaration.ts @@ -1,7 +1,10 @@ +import path from "path" + import ts from "typescript" import { ParseNodeType, ParseState, combine } from "../parse_node" import { Test } from "../tests/test" +import { mockProjectPath } from "../tests/test_utils" import { getImportResPathForEnum } from "./parse_import_declaration" @@ -38,12 +41,10 @@ export const parseEnumDeclaration = ( const enumType = props.program.getTypeChecker().getTypeAtLocation(node) const { resPath, enumName } = getImportResPathForEnum(enumType, props) - const fileName = - props.sourceFileAsset.gdContainingDirectory + - props.sourceFileAsset.name + - "_" + - enumName + - ".gd" + const fileName = props.project.paths.replaceExtension( + props.sourceFileAsset.gdPath, + `_${enumName}.gd` + ) return { content: `const ${enumName} = preload("${resPath}").${enumName}`, @@ -71,17 +72,17 @@ export class Hello { type: "multiple-files", files: [ { - fileName: "/Users/johnfn/MyGame/compiled/Hello.gd", + fileName: mockProjectPath("Hello.gd"), expected: ` class_name Hello -const MyEnum = preload("res://compiled/Test_MyEnum.gd").MyEnum +const MyEnum = preload("res://Test_MyEnum.gd").MyEnum func _ready(): print(MyEnum.A) `, }, { - fileName: "/Users/johnfn/MyGame/compiled/Test_MyEnum.gd", + fileName: mockProjectPath("Test_MyEnum.gd"), expected: ` const MyEnum = { "A": 0, @@ -109,17 +110,17 @@ export class Hello { type: "multiple-files", files: [ { - fileName: "/Users/johnfn/MyGame/compiled/Hello.gd", + fileName: mockProjectPath("Hello.gd"), expected: ` class_name Hello -const TestEnum = preload("res://compiled/Test_TestEnum.gd").TestEnum +const TestEnum = preload("res://Test_TestEnum.gd").TestEnum func _ready(): print(TestEnum.A) `, }, { - fileName: "/Users/johnfn/MyGame/compiled/Test_TestEnum.gd", + fileName: mockProjectPath("Test_TestEnum.gd"), expected: ` const TestEnum = { "A": "A", diff --git a/parse_node/parse_import_declaration.ts b/parse_node/parse_import_declaration.ts index d420ebd5..02605cb1 100644 --- a/parse_node/parse_import_declaration.ts +++ b/parse_node/parse_import_declaration.ts @@ -4,7 +4,7 @@ import { UsageDomain } from "tsutils" import ts, { SyntaxKind } from "typescript" import TsGdProject from "../project/project" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project/errors" import { ParseNodeType, ParseState, combine } from "../parse_node" import { isEnumType } from "../ts_utils" @@ -61,9 +61,11 @@ export const getImportResPathForEnum = ( const enumDeclaration = enumDeclarations[0] const enumSourceFile = enumDeclaration.getSourceFile() + const targetPath = path.normalize(enumSourceFile.fileName) + const enumSourceFileAsset = props.project .sourceFiles() - .find((sf) => sf.fsPath === enumSourceFile.fileName) + .find((sf) => sf.fsPath === targetPath) if (!enumSourceFileAsset) { throw new Error( @@ -77,9 +79,10 @@ export const getImportResPathForEnum = ( enumTypeString = enumTypeString.slice("typeof ".length) } - const pathWithoutEnum = enumSourceFileAsset.resPath - const importPath = - pathWithoutEnum.slice(0, -".gd".length) + "_" + enumTypeString + ".gd" + const importPath = props.project.paths.replaceResExtension( + enumSourceFileAsset.resPath, + `_${enumTypeString}.gd` + ) return { resPath: importPath, @@ -152,7 +155,7 @@ export const parseImportDeclaration = ( continue } - addError({ + props.project.errors.add({ error: ErrorName.InvalidNumber, location: node, description: `Import ${pathToImportedTs} not found.`, diff --git a/parse_node/parse_property_declaration.ts b/parse_node/parse_property_declaration.ts index a5d3e082..3775de23 100644 --- a/parse_node/parse_property_declaration.ts +++ b/parse_node/parse_property_declaration.ts @@ -1,10 +1,11 @@ import chalk from "chalk" import ts, { SyntaxKind } from "typescript" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project/errors" import { ParseNodeType, ParseState, combine } from "../parse_node" import { Test } from "../tests/test" import { getGodotType, getTypeHierarchy, isEnumType } from "../ts_utils" +import { mockProjectPath } from "../tests/test_utils" export const isDecoratedAsExports = ( node: @@ -127,7 +128,7 @@ const parseExportsArrayElement = ( if (typeGodotElement) { godotExportArgs.push(typeGodotElement) } else { - addError({ + props.project.errors.add({ description: ` Cannot infer element type for array export. `, @@ -166,7 +167,7 @@ export const parseExportFlags = ( ) if (!decoration || decoration.expression.kind !== SyntaxKind.CallExpression) { - addError({ + props.project.errors.add({ description: ` I'm confused by export_flags here. It should be a function call. @@ -266,7 +267,7 @@ export const parsePropertyDeclaration = ( if (signalName.startsWith("$")) { signalName = signalName.slice(1) } else { - addError({ + props.project.errors.add({ description: "Signals must be prefixed with $.", error: ErrorName.SignalsMustBePrefixedWith$, location: node, @@ -448,16 +449,16 @@ export class Test { type: "multiple-files", files: [ { - fileName: "/Users/johnfn/MyGame/compiled/Test.gd", + fileName: mockProjectPath("Test.gd"), expected: ` class_name Test -const MyEnum = preload("res://compiled/Test_MyEnum.gd").MyEnum +const MyEnum = preload("res://Test_MyEnum.gd").MyEnum export(MyEnum) var foo `, }, { - fileName: "/Users/johnfn/MyGame/compiled/Test_MyEnum.gd", + fileName: mockProjectPath("Test_MyEnum.gd"), expected: ` const MyEnum = { }`, diff --git a/parse_node/parse_source_file.ts b/parse_node/parse_source_file.ts index a669492b..88441270 100644 --- a/parse_node/parse_source_file.ts +++ b/parse_node/parse_source_file.ts @@ -1,8 +1,11 @@ +import path from "path" + import ts, { SyntaxKind } from "typescript" -import { ErrorName, addError } from "../errors" +import { ErrorName } from "../project" import { ParseNodeType, ParseState, combine, parseNode } from "../parse_node" import { Test } from "../tests/test" +import { mockProjectPath } from "../tests/test_utils" import { LibraryFunctions } from "./library_functions" @@ -48,7 +51,7 @@ export const parseSourceFile = ( const { statements } = node const sourceInfo = props.project .sourceFiles() - .find((file) => file.fsPath === node.fileName) + .find((file) => file.fsPath === path.normalize(node.fileName)) // props.usages = utils.collectVariableUsage(node) props.isAutoload = sourceInfo?.isAutoload() ?? false @@ -62,7 +65,7 @@ export const parseSourceFile = ( ) as ts.ClassDeclaration[] if (allClasses.length === 0) { - addError({ + props.project.errors.add({ error: ErrorName.ClassNameNotFound, location: node, description: @@ -111,7 +114,7 @@ export const parseSourceFile = ( const className = classDecl.name?.text if (!className) { - addError({ + props.project.errors.add({ description: "Anonymous classes are not supported", error: ErrorName.ClassCannotBeAnonymous, location: classDecl, @@ -122,8 +125,7 @@ export const parseSourceFile = ( } parsedClassDeclarations.push({ - fileName: - props.sourceFileAsset.gdContainingDirectory + className + ".gd", + fileName: props.sourceFileAsset.pathForClassname(className), parsedClass: parsedStatement, classDecl, }) @@ -184,7 +186,9 @@ ${parsedClass.content}`, // Generate SOME code - even though it'll certainly be wrong files.push({ - filePath: node.getSourceFile().fileName.slice(0, -".ts".length), + filePath: props.project.paths.removeExtension( + node.getSourceFile().fileName + ), body: ` ${getFileHeader()} ${hoistedEnumImports} @@ -221,11 +225,11 @@ export class Test2 { } type: "multiple-files", files: [ { - fileName: "/Users/johnfn/MyGame/compiled/Test1.gd", + fileName: mockProjectPath("Test1.gd"), expected: `class_name Test1`, }, { - fileName: "/Users/johnfn/MyGame/compiled/Test2.gd", + fileName: mockProjectPath("Test2.gd"), expected: `class_name Test2`, }, ], diff --git a/project/assets/asset_base.ts b/project/assets/asset_base.ts new file mode 100644 index 00000000..a4379a64 --- /dev/null +++ b/project/assets/asset_base.ts @@ -0,0 +1,33 @@ +import TsGdProject from "../project" + +export type AssetConstructor = new ( + fsPath: string, + project: TsGdProject +) => T + +export abstract class AssetBase { + /** e.g. /Users/johnfn/GodotProject/Scenes/my_scene.tscn */ + readonly fsPath: string + + /** e.g. res://Scenes/my_scene.tscn */ + readonly resPath: string + + /** e.g. import('/Users/johnfn/GodotGame/scripts/Enemy').Enemy */ + abstract get tsType(): string | null + + /** file extensions which this asset class should be used for */ + static readonly extensions: string[] = [] + + readonly project: TsGdProject + constructor(fsPath: string, project: TsGdProject, resPath?: string) { + this.project = project + this.fsPath = fsPath + this.resPath = resPath ?? project.paths.fsPathToResPath(fsPath) + } + + tsRelativePath(withExtension = false) { + return this.project.paths.tsRelativePath(this.fsPath, withExtension) + } +} + +export default AssetBase diff --git a/project/assets/asset_font.ts b/project/assets/asset_font.ts index deef3d4a..1c673bc1 100644 --- a/project/assets/asset_font.ts +++ b/project/assets/asset_font.ts @@ -1,24 +1,11 @@ -import TsGdProject from "../project" +import { AssetBase } from "./asset_base" -import { BaseAsset } from "./base_asset" +export class AssetFont extends AssetBase { + static extensions = [".ttf"] -export class AssetFont extends BaseAsset { - resPath: string - fsPath: string - project: TsGdProject - - static extensions() { - return [".ttf"] - } - - constructor(path: string, project: TsGdProject) { - super() - this.fsPath = path - this.resPath = project.paths.fsPathToResPath(this.fsPath) - this.project = project - } - - tsType(): string { + get tsType() { return "DynamicFontData" } } + +export default AssetFont diff --git a/project/assets/asset_glb.ts b/project/assets/asset_glb.ts index 6287e065..3a56d5b7 100644 --- a/project/assets/asset_glb.ts +++ b/project/assets/asset_glb.ts @@ -1,25 +1,11 @@ -import TsGdProject from "../project" +import { AssetBase } from "./asset_base" -import { BaseAsset } from "./base_asset" +export class AssetGlb extends AssetBase { + static extensions = [".glb"] -export class AssetGlb extends BaseAsset { - resPath: string - fsPath: string - project: TsGdProject - - constructor(path: string, project: TsGdProject) { - super() - - this.fsPath = path - this.resPath = project.paths.fsPathToResPath(this.fsPath) - this.project = project - } - - tsType(): string { + get tsType() { return "Spatial" } - - static extensions() { - return [".glb"] - } } + +export default AssetGlb diff --git a/project/godot_project_file.ts b/project/assets/asset_godot_project_file.ts similarity index 88% rename from project/godot_project_file.ts rename to project/assets/asset_godot_project_file.ts index 749ccee0..22e4315c 100644 --- a/project/godot_project_file.ts +++ b/project/assets/asset_godot_project_file.ts @@ -1,7 +1,9 @@ import fs from "fs" -import { parseGodotConfigFile } from "./godot_parser" -import TsGdProject from "./project" +import { parseGodotConfigFile } from "../godot_parser" +import TsGdProject from "../project" + +import { AssetBase } from "./asset_base" interface IRawGodotConfig { globals: { @@ -42,21 +44,19 @@ interface IRawGodotConfig { } } -export class GodotProjectFile { +export class AssetGodotProjectFile extends AssetBase { + static extensions = [".godot"] + rawConfig: IRawGodotConfig autoloads: { resPath: string }[] - fsPath: string actionNames: string[] - project: TsGdProject - constructor(path: string, project: TsGdProject) { - this.rawConfig = parseGodotConfigFile(path, { + constructor(fsPath: string, project: TsGdProject) { + super(fsPath, project) + this.rawConfig = parseGodotConfigFile(fsPath, { autoload: [], }) as IRawGodotConfig - this.project = project - this.fsPath = path - this.autoloads = Object.values(this.rawConfig.autoload[0] ?? {}) .filter((x) => typeof x === "string") .map((x) => ({ @@ -142,4 +142,10 @@ export class GodotProjectFile { fsPath: this.project.paths.resPathToFsPath(mainSceneResPath), } } + + get tsType() { + return null as never + } } + +export default AssetGodotProjectFile diff --git a/project/assets/asset_godot_scene.ts b/project/assets/asset_godot_scene.ts index 7e82087c..c791b016 100644 --- a/project/assets/asset_godot_scene.ts +++ b/project/assets/asset_godot_scene.ts @@ -2,13 +2,13 @@ import path from "path" import chalk from "chalk" -import { ErrorName, addError } from "../../errors" +import { ErrorName } from "../errors" import TsGdProject from "../project" import { parseGodotConfigFile } from "../godot_parser" import { AssetGlb } from "./asset_glb" import { AssetSourceFile } from "./asset_source_file" -import { BaseAsset } from "./base_asset" +import { AssetBase } from "./asset_base" interface IGodotSceneFile { gd_scene: { @@ -174,13 +174,13 @@ export class GodotNode { return !this._type && !this.isInstance() } - tsType(): string { + get tsType(): string { if (this._type) { return this._type } if (this.isInstanceOverride()) { - addError({ + this.project.errors.add({ description: `Error: should never try to get the type of an instance override. This is a ts2gd internal bug. Please report it on GitHub.`, error: ErrorName.InvalidFile, location: this.name, @@ -190,13 +190,13 @@ export class GodotNode { return "any" } - const instancedSceneType = this.instance()?.tsType() + const instancedSceneType = this.instance()?.tsType if (instancedSceneType) { return instancedSceneType } - addError({ + this.project.errors.add({ description: `Error: Your Godot scene ${chalk.blue( this.scene.fsPath )} refers to ${chalk.red( @@ -259,12 +259,8 @@ type ResourceTemp = { id: number } -export class AssetGodotScene extends BaseAsset { - /** e.g. /Users/johnfn/GodotProject/Scenes/my_scene.tscn */ - fsPath: string - - /** e.g. res://Scenes/my_scene.tscn */ - resPath: string +export class AssetGodotScene extends AssetBase { + static extensions = [".tscn"] nodes: GodotNode[] @@ -275,20 +271,14 @@ export class AssetGodotScene extends BaseAsset { rootNode: GodotNode - project: TsGdProject - constructor(fsPath: string, project: TsGdProject) { - super() + super(fsPath, project) const sceneFile = parseGodotConfigFile(fsPath, { ext_resource: [], node: [], }) as IGodotSceneFile - this.fsPath = fsPath - this.project = project - this.resPath = project.paths.fsPathToResPath(fsPath) - this.resources = (sceneFile.ext_resource ?? []).map((resource) => { return { resPath: resource.$section.path, @@ -305,8 +295,7 @@ export class AssetGodotScene extends BaseAsset { this.rootNode = this.nodes.find((node) => !node.parent)! } - /** e.g. import('/Users/johnfn/GodotGame/scripts/Enemy').Enemy */ - tsType(): string { + get tsType(): string { const rootScript = this.rootNode.getScript() if (rootScript) { @@ -315,7 +304,7 @@ export class AssetGodotScene extends BaseAsset { .find((sf) => sf.resPath === rootScript.resPath) if (!rootSourceFile) { - addError({ + this.project.errors.add({ description: `Failed to find root source file for ${rootScript.fsPath}`, error: ErrorName.Ts2GdError, location: rootScript.fsPath, @@ -328,7 +317,7 @@ export class AssetGodotScene extends BaseAsset { const className = rootSourceFile.exportedTsClassName() if (!className) { - addError({ + this.project.errors.add({ description: `Failed to find classname for ${rootScript.fsPath}`, error: ErrorName.Ts2GdError, location: rootScript.fsPath, @@ -338,16 +327,15 @@ export class AssetGodotScene extends BaseAsset { return "any" } - return `import('${rootSourceFile.fsPath.slice( - 0, - -".ts".length - )}').${rootSourceFile.exportedTsClassName()}` + return rootSourceFile.tsType } else { - return this.rootNode.tsType() + return this.rootNode.tsType } } +} - static extensions() { - return [".tscn"] - } +export default AssetGodotScene + +export function isAssetGodotScene(input: object): input is AssetGodotScene { + return input instanceof AssetGodotScene } diff --git a/project/assets/asset_image.ts b/project/assets/asset_image.ts index 3c4c60c1..0c2fd78b 100644 --- a/project/assets/asset_image.ts +++ b/project/assets/asset_image.ts @@ -1,25 +1,11 @@ -import TsGdProject from "../project" +import { AssetBase } from "./asset_base" -import { BaseAsset } from "./base_asset" +export class AssetImage extends AssetBase { + static extensions = [".gif", ".png", ".jpg", ".bmp"] -export class AssetImage extends BaseAsset { - resPath: string - fsPath: string - project: TsGdProject - - constructor(path: string, project: TsGdProject) { - super() - - this.fsPath = path - this.resPath = project.paths.fsPathToResPath(this.fsPath) - this.project = project - } - - tsType(): string { + get tsType() { return "StreamTexture" } - - static extensions() { - return [".gif", ".png", ".jpg", ".bmp"] - } } + +export default AssetImage diff --git a/project/assets/asset_source_file.ts b/project/assets/asset_source_file.ts index c6d37aa6..ea7835f5 100644 --- a/project/assets/asset_source_file.ts +++ b/project/assets/asset_source_file.ts @@ -5,77 +5,61 @@ import ts, { SyntaxKind } from "typescript" import * as utils from "tsutils" import chalk from "chalk" -import { ErrorName, TsGdError, addError } from "../../errors" +import { ErrorName, TsGdError } from "../errors" import { Scope } from "../../scope" import TsGdProject from "../project" import { parseNode } from "../../parse_node" -import { BaseAsset } from "./base_asset" +import { AssetBase } from "./asset_base" // TODO: We currently allow for invalid states (e.g. className() is undefined) // because we only create AssetSourceFiles on a chokidar 'add' operation (we // dont make them on edit). // Can we just create them on edit as well (if it doesn't exist but is valid)? -export class AssetSourceFile extends BaseAsset { - /** Like "res://src/main.gd" */ - resPath: string +export class AssetSourceFile extends AssetBase { + static extensions = [".ts"] - /** Like "/Users/johnfn/GodotProject/compiled/main.gd" */ - gdPath: string + readonly gdClassName: string - /** Like "/Users/johnfn/GodotProject/compiled/ " */ - gdContainingDirectory: string - - /** Like "/Users/johnfn/GodotProject/src/main.ts" */ - fsPath: string - - name: string + /** Like "/Users/johnfn/GodotProject/compiled/main.gd" + * Beware that this only is correct if the className corresponds to the file name + * Should be refactored, maybe add the contained classnames to the Asset + * @deprecated + */ + readonly gdPath: string - /** Like "src/main.ts" */ - tsRelativePath: string + /** Like "/Users/johnfn/GodotProject/compiled/ " */ + readonly gdContainingDirectory: string - /** - * List of all files that were written when compiling this source file. - */ + /** List of all files that were written when compiling this source file. */ writtenFiles: string[] = [] - project: TsGdProject - private _isAutoload: boolean constructor(sourceFilePath: string, project: TsGdProject) { - super() - - let gdPath = path.join( - project.paths.destGdPath, - sourceFilePath.slice( - project.paths.sourceTsPath.length, - -path.extname(sourceFilePath).length - ) + ".gd" - ) + // swap in the gdPath for the tsPath as resPath + const gdPath = project.paths.gdPathForTs(sourceFilePath) + + super(sourceFilePath, project, project.paths.fsPathToResPath(gdPath)) - this.resPath = project.paths.fsPathToResPath(gdPath) this.gdPath = gdPath - this.gdContainingDirectory = gdPath.slice(0, gdPath.lastIndexOf("/") + 1) - this.fsPath = sourceFilePath - this.tsRelativePath = sourceFilePath.slice( - project.paths.rootPath.length + 1 - ) - this.name = this.gdPath.slice( - this.gdContainingDirectory.length, - -".gd".length - ) - this.project = project + this.gdClassName = project.paths.gdName(this.gdPath) + this.gdContainingDirectory = path.dirname(this.gdPath) + this._isAutoload = !!this.project.godotProject.autoloads.find( (a) => a.resPath === this.resPath ) } + pathForClassname(className: string) { + return path.join(this.gdContainingDirectory, `${className}.gd`) + } + reload() {} private getAst(): TsGdError | ts.SourceFile { - const ast = this.project.program.getProgram().getSourceFile(this.fsPath) + const ast = this.project.program.getSourceFile(this.fsPath) if (!ast) { return { @@ -141,7 +125,7 @@ This is a ts2gd bug. Please create an issue on GitHub for it.`, if (!name) { return { error: ErrorName.ClassCannotBeAnonymous, - location: node ?? this.tsRelativePath, + location: node ?? this.tsRelativePath(true), description: "This class cannot be anonymous", stack: new Error().stack ?? "", } @@ -206,7 +190,7 @@ Hint: try ${chalk.blueBright( // TODO: Error could say the exact loc to write return { error: ErrorName.CantFindAutoloadInstance, - location: ast ?? this.tsRelativePath, + location: ast ?? this.tsRelativePath(true), stack: new Error().stack ?? "", description: `Can't find the autoload instance variable for this autoload class. All files with an autoload class must export an instance of that class. Here's an example: @@ -226,21 +210,21 @@ ${chalk.green( return this._isAutoload } - tsType(): string { + get tsType() { const className = this.exportedTsClassName() if (className) { - return `import('${this.fsPath.slice(0, -".ts".length)}').${className}` - } else { - addError({ - description: `Failed to find className for ${this.fsPath}`, - error: ErrorName.Ts2GdError, - location: this.fsPath, - stack: new Error().stack ?? "", - }) - - return "any" + return `import('${this.tsRelativePath()}').${className}` } + + this.project.errors.add({ + description: `Failed to find className for ${this.fsPath}`, + error: ErrorName.Ts2GdError, + location: this.fsPath, + stack: new Error().stack ?? "", + }) + + return "any" } private isProjectAutoload(): boolean { @@ -300,9 +284,9 @@ ${chalk.green( for (const theirFile of sf.writtenFiles) { for (const ourFile of this.writtenFiles) { if (theirFile === ourFile) { - addError({ + this.project.errors.add({ description: `You have two classes named ${ - this.name + this.gdClassName } in the same folder. ts2gd saves every class as "class_name.gd", so they will overwrite each other. We recommend renaming one, but you can also move it into a new directory. First path: ${chalk.yellow(this.fsPath)} @@ -319,13 +303,11 @@ Second path: ${chalk.yellow(sf.fsPath)}`, } } - async compile( - watchProgram: ts.WatchOfConfigFile - ): Promise { + async compile(watchProgram: ts.Program): Promise { const oldAutoloadClassName = this.getAutoloadNameFromExportedVariable() let fsContent = await fs.readFile(this.fsPath, "utf-8") - let sourceFileAst = watchProgram.getProgram().getSourceFile(this.fsPath) + let sourceFileAst = watchProgram.getSourceFile(this.fsPath) let tries = 0 while ( @@ -337,14 +319,14 @@ Second path: ${chalk.yellow(sf.fsPath)}`, ++tries < 50 ) { await new Promise((resolve) => setTimeout(resolve, 10)) - sourceFileAst = watchProgram.getProgram().getSourceFile(this.fsPath) + sourceFileAst = watchProgram.getSourceFile(this.fsPath)! if (sourceFileAst) { fsContent = await fs.readFile(this.fsPath, "utf-8") } } if (!sourceFileAst) { - addError({ + this.project.errors.add({ description: `TS can't find source file ${this.fsPath} after waiting 0.5 second. Try saving your TypeScript file again.`, error: ErrorName.PathNotFound, location: this.fsPath, @@ -357,18 +339,18 @@ Second path: ${chalk.yellow(sf.fsPath)}`, const parsedNode = parseNode(sourceFileAst, { indent: "", isConstructor: false, - scope: new Scope(watchProgram.getProgram().getProgram()), + scope: new Scope(watchProgram), project: this.project, mostRecentControlStructureIsSwitch: false, isAutoload: this.isProjectAutoload(), - program: watchProgram.getProgram().getProgram(), + program: watchProgram, usages: utils.collectVariableUsage(sourceFileAst), sourceFile: sourceFileAst, sourceFileAsset: this, }) // TODO: Only do this once per program run max! - await fs.mkdir(path.dirname(this.gdPath), { recursive: true }) + await fs.mkdir(this.gdContainingDirectory, { recursive: true }) this.writtenFiles = [] @@ -385,7 +367,7 @@ Second path: ${chalk.yellow(sf.fsPath)}`, const error = this.validateAutoloadClass() if (error !== null) { - addError(error) + this.project.errors.add(error) } const newAutoloadClassName = this.getAutoloadNameFromExportedVariable() @@ -422,7 +404,7 @@ Second path: ${chalk.yellow(sf.fsPath)}`, description: `Be sure to export an instance of your autoload class, e.g.: ${chalk.white( - `export const ${this.getGodotClassName()} = new ${this.exportedTsClassName()}()` + `export const ${this.gdClassName} = new ${this.exportedTsClassName()}()` )} `, location: classNode ?? this.fsPath, @@ -433,10 +415,6 @@ ${chalk.white( return null } - getGodotClassName(): string { - return this.fsPath.slice(this.fsPath.lastIndexOf("/") + 1, -".ts".length) - } - checkForAutoloadChanges(): void { let shouldBeAutoload: boolean let prevAutoload = this.isAutoload() @@ -467,7 +445,7 @@ ${chalk.white( typeof autoloadClassName !== "string" && "error" in autoloadClassName ) { - addError(autoloadClassName) + this.project.errors.add(autoloadClassName) return } @@ -480,7 +458,7 @@ ${chalk.white( const classNode = this.getClassNode() - addError({ + this.project.errors.add({ error: ErrorName.AutoloadProjectButNotDecorated, description: `Since this is an autoload class in Godot, you must put ${chalk.white( "@autoload" @@ -503,7 +481,7 @@ ${chalk.white( const classNode = this.getClassNode() - addError({ + this.project.errors.add({ error: ErrorName.AutoloadDecoratedButNotProject, description: `Since you removed this as an autoload class in Godot, you must remove ${chalk.white( "@autoload" @@ -527,7 +505,7 @@ ${chalk.white( // Delete the generated enum files const filesInDirectory = await fs.readdir(this.gdContainingDirectory) - const nameWithoutExtension = this.gdPath.slice(0, -".gd".length) + const nameWithoutExtension = this.project.paths.removeExtension(this.gdPath) for (const fileName of filesInDirectory) { const fullPath = this.gdContainingDirectory + fileName @@ -540,3 +518,9 @@ ${chalk.white( this.project.godotProject.removeAutoload(this.resPath) } } + +export default AssetSourceFile + +export function isAssetSourceFile(input: object): input is AssetSourceFile { + return input instanceof AssetSourceFile +} diff --git a/project/assets/asset_utils.ts b/project/assets/asset_utils.ts index 5e2bcb09..11eeafd0 100644 --- a/project/assets/asset_utils.ts +++ b/project/assets/asset_utils.ts @@ -1,14 +1,15 @@ -import { AssetFont } from "./asset_font" -import { AssetGlb } from "./asset_glb" -import { AssetGodotScene } from "./asset_godot_scene" -import { AssetImage } from "./asset_image" +import AssetFont from "./asset_font" +import AssetGlb from "./asset_glb" +import AssetGodotProjectFile from "./asset_godot_project_file" +import AssetGodotScene from "./asset_godot_scene" +import AssetImage from "./asset_image" -export function allAssetExtensions() { +export function allNonTsAssetExtensions() { return [ - ...AssetFont.extensions(), - ...AssetImage.extensions(), - ...AssetGodotScene.extensions(), - ...AssetGlb.extensions(), - ".godot", + ...AssetFont.extensions, + ...AssetImage.extensions, + ...AssetGodotScene.extensions, + ...AssetGlb.extensions, + ...AssetGodotProjectFile.extensions, ] } diff --git a/project/assets/base_asset.ts b/project/assets/base_asset.ts deleted file mode 100644 index 0b606def..00000000 --- a/project/assets/base_asset.ts +++ /dev/null @@ -1,7 +0,0 @@ -export abstract class BaseAsset { - abstract resPath: string - - abstract fsPath: string - - abstract tsType(): string | null -} diff --git a/project/assets/index.ts b/project/assets/index.ts index cbfc72a8..de2494e5 100644 --- a/project/assets/index.ts +++ b/project/assets/index.ts @@ -1 +1,9 @@ +export * from "./asset_base" +export { default } from "./asset_base" +export * from "./asset_font" +export * from "./asset_glb" +export * from "./asset_godot_project_file" +export * from "./asset_godot_scene" +export * from "./asset_image" +export * from "./asset_source_file" export * from "./asset_utils" diff --git a/project/errors.ts b/project/errors.ts new file mode 100644 index 00000000..813243ad --- /dev/null +++ b/project/errors.ts @@ -0,0 +1,111 @@ +import chalk from "chalk" +import ts from "typescript" + +import { ParsedArgs } from "../parse_args" + +export enum ErrorName { + InvalidNumber, + InvalidImport, + ClassNameNotFound, + ClassDoesntExtendAnything, + ClassMustBeExported, + TooManyClassesFound, + ClassCannotBeAnonymous, + TwoClassesWithSameName, + CantFindAutoloadInstance, + UnknownTsSyntax, + PathNotFound, + ExportedVariableError, + InvalidFile, + + Ts2GdError, + + AutoloadProjectButNotDecorated, + AutoloadDecoratedButNotProject, + AutoloadNotExported, + + NoComplicatedConnect, + + SignalsMustBePrefixedWith$, + + DeclarationNotGiven, +} + +export type TsGdError = { + error: ErrorName + location: ts.Node | string + stack: string + description: string +} + +export class Errors { + private errors: TsGdError[] = [] + + constructor(private args: ParsedArgs) {} + + get() { + return this.errors + } + + add(error: TsGdError) { + this.errors.push(error) + } + + display(message: string) { + if (!this.args.debug) { + console.clear() + } + + if (this.errors.length === 0) { + console.info(message) + console.info() + console.info(chalk.greenBright("No errors.")) + + return false + } + + console.info(message) + console.info() + console.info( + chalk.redBright( + `${this.errors.length} error${this.errors.length > 1 ? "s" : ""}.` + ) + ) + + for (const error of this.errors) { + if (typeof error.location === "string") { + console.warn(`${chalk.blueBright(error.location)}`) + } else { + const lineAndChar = error.location + .getSourceFile() + ?.getLineAndCharacterOfPosition(error.location.getStart()) + + if (!lineAndChar && this.args.debug) { + console.log(lineAndChar) + console.log(error.location) + console.log(error.stack) + } + + const { line, character } = lineAndChar + + console.warn() + console.warn( + `${chalk.blueBright( + error.location.getSourceFile().fileName + )}:${chalk.yellow(line + 1)}:${chalk.yellow(character + 1)}` + ) + + if (this.args.debug) { + console.log(error.stack) + } + } + + console.info(error.description) + } + + this.errors = [] + return true + } +} + +export default Errors diff --git a/project/generate_dynamic_defs/build_asset_paths.ts b/project/generate_dynamic_defs/build_asset_paths.ts index 772bf9e2..101344dd 100644 --- a/project/generate_dynamic_defs/build_asset_paths.ts +++ b/project/generate_dynamic_defs/build_asset_paths.ts @@ -1,30 +1,27 @@ import fs from "fs" import path from "path" -import { AssetGodotScene } from "../assets/asset_godot_scene" -import { AssetSourceFile } from "../assets/asset_source_file" +import { isAssetGodotScene, isAssetSourceFile } from "../assets" import TsGdProject from "../project" export default function buildAssetPathsType(project: TsGdProject) { const assetFileContents = ` declare type AssetType = { ${project.assets - .filter((obj) => obj.tsType() !== null) + .filter((obj) => obj.tsType !== null) .map((obj) => { - const tsType = obj.tsType() - - if (obj instanceof AssetSourceFile || obj instanceof AssetGodotScene) { - return ` '${obj.resPath}': PackedScene<${tsType}>` + if (isAssetSourceFile(obj) || isAssetGodotScene(obj)) { + return ` '${obj.resPath}': PackedScene<${obj.tsType}>` } - return ` '${obj.resPath}': ${tsType}` + return ` '${obj.resPath}': ${obj.tsType}` }) .join(",\n")} } declare type SceneName = ${project.assets - .filter((obj): obj is AssetGodotScene => obj instanceof AssetGodotScene) + .filter(isAssetGodotScene) .map((obj) => ` | '${obj.resPath}'`) .join("\n")} diff --git a/project/generate_dynamic_defs/build_group_types.ts b/project/generate_dynamic_defs/build_group_types.ts index 753fe45f..ef0a6f5a 100644 --- a/project/generate_dynamic_defs/build_group_types.ts +++ b/project/generate_dynamic_defs/build_group_types.ts @@ -1,7 +1,7 @@ import fs from "fs" import path from "path" -import { TsGdError } from "../../errors" +import { TsGdError } from "../errors" import TsGdProject from "../project" export default function buildGroupTypes(project: TsGdProject): void { @@ -12,7 +12,7 @@ export default function buildGroupTypes(project: TsGdProject): void { for (const group of node.groups) { groupNameToTypes[group] ??= new Set() - const result = node.tsType() + const result = node.tsType groupNameToTypes[group].add(result) } diff --git a/project/generate_dynamic_defs/build_node_paths.ts b/project/generate_dynamic_defs/build_node_paths.ts index c9772361..0d853478 100644 --- a/project/generate_dynamic_defs/build_node_paths.ts +++ b/project/generate_dynamic_defs/build_node_paths.ts @@ -56,7 +56,6 @@ export default function buildNodePathsTypeForScript( for (const scene of project.godotScenes()) { for (const node of scene.nodes) { const nodeScript = node.getScript() - if (nodeScript && nodeScript.resPath === script.resPath) { const instance = node.instance() let isValid = false @@ -186,16 +185,7 @@ export default function buildNodePathsTypeForScript( const pathToImport: { [key: string]: string } = {} for (const { path, node } of commonRelativePaths) { - const script = node.getScript() - - if (script) { - pathToImport[path] = `import("${script.fsPath.slice( - 0, - -".ts".length - )}").${script.exportedTsClassName()}` - } else { - pathToImport[path] = node.tsType() - } + pathToImport[path] = node.getScript()?.tsType ?? node.tsType } type RecursivePath = { @@ -283,9 +273,9 @@ ${Object.entries(pathToImport) result += ` -import { ${className} } from '${script.tsRelativePath.slice(0, -".ts".length)}' +import { ${className} } from '${script.tsRelativePath()}' -declare module '${script.tsRelativePath.slice(0, -".ts".length)}' { +declare module '${script.tsRelativePath()}' { enum ADD_A_GENERIC_TYPE_TO_GET_NODE_FOR_THIS_TO_WORK {} interface ${className} { @@ -325,7 +315,7 @@ declare module '${script.tsRelativePath.slice(0, -".ts".length)}' { const destPath = path.join( project.paths.dynamicGodotDefsPath, - `@node_paths_${script.getGodotClassName()}.d.ts` + `@node_paths_${script.gdClassName}.d.ts` ) fs.writeFileSync(destPath, result) diff --git a/project/generate_dynamic_defs/build_scene_imports.ts b/project/generate_dynamic_defs/build_scene_imports.ts index 55cffd88..7dd03303 100644 --- a/project/generate_dynamic_defs/build_scene_imports.ts +++ b/project/generate_dynamic_defs/build_scene_imports.ts @@ -12,7 +12,7 @@ export default function buildSceneImports(project: TsGdProject) { result += `export const ${path.basename( scene.fsPath, ".tscn" - )}Tscn: PackedScene<${scene.tsType() ?? "Node"}>;\n` + )}Tscn: PackedScene<${scene.tsType ?? "Node"}>;\n` } const destPath = path.join(project.paths.dynamicGodotDefsPath, "@scenes.d.ts") diff --git a/project/index.ts b/project/index.ts index 1a7f867f..2f052723 100644 --- a/project/index.ts +++ b/project/index.ts @@ -1,3 +1,4 @@ export { Paths } from "./paths" export { TsGdProject } from "./project" export { default } from "./project" +export * from "./errors" diff --git a/project/paths.ts b/project/paths.ts index 5fc9e159..d6d1ed5e 100644 --- a/project/paths.ts +++ b/project/paths.ts @@ -8,7 +8,7 @@ import { ParsedArgs } from "../parse_args" import { defaultTsconfig } from "../generate_library_defs/generate_tsconfig" import { showLoadingMessage } from "../main" -import { allAssetExtensions } from "./assets" +import { allNonTsAssetExtensions } from "./assets" // TODO: Do sourceTsPath and destGdPath have to be relative? @@ -44,15 +44,65 @@ export class Paths { readonly gdscriptPath: string - additionalIgnores: string[] - tsFileIgnores: string[] + readonly additionalIgnores: string[] + readonly tsFileIgnores: string[] resPathToFsPath(resPath: string) { - return path.join(this.rootPath, resPath.slice("res://".length)) + return path.normalize( + path.join(this.rootPath, resPath.slice("res://".length)) + ) + } + + normalizeToPosix(fsPath: string) { + return path.normalize(fsPath).replaceAll(path.sep, path.posix.sep) } fsPathToResPath(fsPath: string) { - return "res://" + fsPath.slice(this.rootPath.length + 1) + return `res://${this.normalizeToPosix( + path.relative(this.rootPath, fsPath) + )}` + } + + replaceResExtension(resPath: string, replacement: string) { + return resPath.replace(/\.[0-9a-z]+$/i, replacement) + } + + removeExtension(fsPath: string) { + if (fsPath.startsWith("res://")) { + throw new Error("removeExtension isn't supported for res:// paths") + } + return path.join( + path.dirname(fsPath), + path.basename(fsPath, path.extname(fsPath)) + ) + } + + replaceExtension(fsPath: string, replacement: string) { + return `${this.removeExtension(fsPath)}${replacement}` + } + + tsRelativePath(fsPath: string, withExtension = false) { + // on win this would have \, but we need / + return this.normalizeToPosix( + path.relative( + path.dirname(this.tsconfigPath), + withExtension ? fsPath : this.removeExtension(fsPath) + ) + ) + } + + gdPathForTs(sourceFilePath: string) { + return path.join( + this.destGdPath, + this.replaceExtension( + path.relative(this.sourceTsPath, sourceFilePath), + ".gd" + ) + ) + } + + gdName(gdPath: string) { + return path.basename(gdPath, path.extname(gdPath)) } ignoredPaths(): Matcher { @@ -69,7 +119,7 @@ export class Paths { // also exclude ts files from the ignore field in the ts2gd.json file `!**/!(*.d${this.tsFileIgnores.map((ignore) => `|${ignore}`)}).ts`, // and don't ignore the following assets - ...allAssetExtensions().map((ext) => `!**/*${ext}`), + ...allNonTsAssetExtensions().map((ext) => `!**/*${ext}`), ] } @@ -122,6 +172,7 @@ export class Paths { fullyQualifiedTs2gdPath = path.dirname(fullyQualifiedTs2gdPathWithFilename) + //TODO: type this const tsgdJson = JSON.parse( fs.readFileSync(fullyQualifiedTs2gdPathWithFilename, "utf-8") ) @@ -137,7 +188,10 @@ export class Paths { "dynamic" ) - this.godotSourceRepoPath = tsgdJson.godotSourceRepoPath || undefined + this.godotSourceRepoPath = + (tsgdJson.godotSourceRepoPath && + path.join(fullyQualifiedTs2gdPath, tsgdJson.godotSourceRepoPath)) || + undefined this.csgClassesPath = path.join( this.godotSourceRepoPath ?? "", diff --git a/project/project.ts b/project/project.ts index a543fcf9..6f7c2a20 100644 --- a/project/project.ts +++ b/project/project.ts @@ -1,24 +1,43 @@ import fs from "fs" import path from "path" +import _ from "lodash" import chalk from "chalk" import chokidar from "chokidar" -import ts from "typescript" +import ts, { parseJsonText } from "typescript" import LibraryBuilder from "../generate_library_defs" import { ParsedArgs } from "../parse_args" -import { displayErrors, TsGdError } from "../errors" -import { GodotProjectFile } from "./godot_project_file" +import Errors, { TsGdError } from "./errors" +import { + AssetBase, + AssetGodotProjectFile, + AssetFont, + AssetGlb, + AssetGodotScene, + AssetImage, + AssetSourceFile, + AssetConstructor, +} from "./assets" import { Paths } from "./paths" -import { AssetFont } from "./assets/asset_font" -import { AssetGlb } from "./assets/asset_glb" -import { AssetGodotScene } from "./assets/asset_godot_scene" -import { AssetImage } from "./assets/asset_image" -import { AssetSourceFile } from "./assets/asset_source_file" -import { BaseAsset } from "./assets/base_asset" import DefinitionBuilder from "./generate_dynamic_defs" +// not static to wait for module load +const AssetLookup = _.memoize(() => + [ + AssetFont, + AssetGlb, + AssetGodotProjectFile, + AssetGodotScene, + AssetImage, + AssetSourceFile, + ].reduce((acc, current) => { + current.extensions.forEach((ext) => acc.set(ext, current)) + return acc + }, new Map>()) +) + // TODO: Instead of manually scanning to find all assets, i could just import // all godot files, and then parse them for all their asset types. It would // probably be easier to find the tscn and tres files. @@ -28,10 +47,10 @@ export class TsGdProject { readonly paths: Paths /** Master list of all Godot assets */ - assets: BaseAsset[] = [] + assets: AssetBase[] = [] /** Parsed project.godot file. */ - godotProject: GodotProjectFile + godotProject: AssetGodotProjectFile /** Each source file. */ sourceFiles(): AssetSourceFile[] { @@ -64,102 +83,102 @@ export class TsGdProject { mainScene: AssetGodotScene - program: ts.WatchOfConfigFile + //program: ts.WatchOfConfigFile + program: ts.Program - args: ParsedArgs + public readonly args: ParsedArgs - definitionBuilder = new DefinitionBuilder(this) + public readonly definitionBuilder: DefinitionBuilder - constructor( - watcher: chokidar.FSWatcher, - initialFilePaths: string[], - program: ts.WatchOfConfigFile, - ts2gdJson: Paths, + public readonly errors: Errors + + constructor(options: { + watcher?: chokidar.FSWatcher + initialFilePaths: string[] + program: ts.Program + ts2gdJson: Paths args: ParsedArgs - ) { + }) { // Initial set up - this.args = args - this.paths = ts2gdJson - this.program = program + this.args = options.args + this.paths = options.ts2gdJson + this.program = options.program - // Parse assets + this.errors = new Errors(this.args) - const projectGodot = initialFilePaths.filter((path) => - path.includes("project.godot") - )[0] + this.definitionBuilder = new DefinitionBuilder(this) - this.godotProject = this.createAsset(projectGodot)! as GodotProjectFile + // Parse assets - const initialAssets = initialFilePaths.map((path) => this.createAsset(path)) + const [projectFilePaths, otherAssetPaths] = _.partition( + options.initialFilePaths, + (path) => path.includes("project.godot") + ) - for (const asset of initialAssets) { - if (asset === null) { - continue + if (projectFilePaths.length !== 1) { + throw new Error( + `Need exactly one project.godot file, but found ${projectFilePaths.length}!` + ) + } else { + const project = this.createGodotProject(projectFilePaths[0]) + if (project) { + this.godotProject = project + } else { + throw new Error( + `Couldn't parse godot project from ${projectFilePaths[0]}` + ) } + } - if (asset instanceof BaseAsset) { - this.assets.push(asset) - } + this.assets = _.compact( + otherAssetPaths.map((path) => this.createAsset(path)) + ) - if (asset instanceof GodotProjectFile) { - this.godotProject = asset - } + const mainScene = this.godotScenes().find( + (scene) => scene.resPath === this.godotProject.mainScene().resPath + ) + + if (!mainScene) { + throw new Error("Main scene not found, check your Godot project!") } - this.mainScene = this.godotScenes().find( - (scene) => scene.resPath === this.godotProject.mainScene().resPath - )! + this.mainScene = mainScene - this.monitor(watcher) + this.monitor(options.watcher) } - createAsset( - path: string - ): - | AssetSourceFile - | AssetGodotScene - | AssetFont - | AssetImage - | GodotProjectFile - | AssetGlb - | null { - //TODO: move these checks to the asset classes in static methods - if (path.endsWith(".ts")) { - return new AssetSourceFile(path, this) - } else if (path.endsWith(".tscn")) { - return new AssetGodotScene(path, this) - } else if (path.endsWith(".godot")) { - return new GodotProjectFile(path, this) - } else if (path.endsWith(".ttf")) { - return new AssetFont(path, this) - } else if (path.endsWith(".glb")) { - return new AssetGlb(path, this) - } else if ( - path.endsWith(".png") || - path.endsWith(".gif") || - path.endsWith(".bmp") || - path.endsWith(".jpg") - ) { - return new AssetImage(path, this) + createGodotProject(path: string): AssetGodotProjectFile | null { + if (path.endsWith(".godot")) { + return new AssetGodotProjectFile(path, this) } console.error(`unhandled asset type ${path}`) + return null + } + createAsset(fsPath: string) { + const ext = path.extname(fsPath) + const constructor = AssetLookup().get(ext) + if (constructor) { + return new constructor(fsPath, this) + } + + console.error(`unhandled asset type ${fsPath}`) return null } - monitor(watcher: chokidar.FSWatcher) { + monitor(watcher?: chokidar.FSWatcher) { watcher - .on("add", async (path) => { + ?.on("add", async (path) => { const message = await this.onAddAsset(path) - displayErrors(this.args, message) + this.errors.display(message) }) .on("change", async (path) => { const message = await this.onChangeAsset(path) - displayErrors(this.args, message) + this.errors.display(message) }) .on("unlink", async (path) => { await this.onRemoveAsset(path) @@ -171,7 +190,7 @@ export class TsGdProject { // Do this first because some assets expect themselves to exist - e.g. // an enum inside a source file expects that source file to exist. - if (newAsset instanceof BaseAsset) { + if (newAsset instanceof AssetBase) { this.assets.push(newAsset) } @@ -214,7 +233,7 @@ export class TsGdProject { if (path.endsWith(".godot")) { const oldProjectFile = this.godotProject - this.godotProject = new GodotProjectFile(path, this) + this.godotProject = new AssetGodotProjectFile(path, this) const oldAutoloads = oldProjectFile.autoloads const newAutoloads = this.godotProject.autoloads @@ -227,26 +246,25 @@ export class TsGdProject { await script.compile(this.program) } } - } - - let oldAsset = this.assets.find((asset) => asset.fsPath === path) - - if (oldAsset) { - let newAsset = this.createAsset(path) as any as BaseAsset - this.assets = this.assets.filter((a) => a.fsPath !== path) - this.assets.push(newAsset) - - if (newAsset instanceof AssetSourceFile) { - await newAsset.compile(this.program) - - this.definitionBuilder.buildAssetPathsType() - this.definitionBuilder.buildNodePathsTypeForScript(newAsset) - } else if (newAsset instanceof AssetGodotScene) { - for (const script of this.sourceFiles()) { - this.definitionBuilder.buildNodePathsTypeForScript(script) + } else { + let oldAsset = this.assets.find((asset) => asset.fsPath === path) + + if (oldAsset) { + let newAsset = this.createAsset(path) as any as AssetBase + this.assets = this.assets.filter((a) => a.fsPath !== path) + this.assets.push(newAsset) + + if (newAsset instanceof AssetSourceFile) { + await newAsset.compile(this.program) + this.definitionBuilder.buildAssetPathsType() + this.definitionBuilder.buildNodePathsTypeForScript(newAsset) + } else if (newAsset instanceof AssetGodotScene) { + for (const script of this.sourceFiles()) { + this.definitionBuilder.buildNodePathsTypeForScript(script) + } + + this.definitionBuilder.buildSceneImports() } - - this.definitionBuilder.buildSceneImports() } } @@ -286,7 +304,7 @@ export class TsGdProject { await Promise.all( assetsToCompile.map((asset) => asset.compile(this.program)) ) - return !displayErrors(this.args, "Compiling all source files...") + return !this.errors.display("Compiling all source files...") } shouldBuildLibraryDefinitions(flags: ParsedArgs) { @@ -325,7 +343,7 @@ export class TsGdProject { export const makeTsGdProject = async ( ts2gdJson: Paths, - program: ts.WatchOfConfigFile, + program: ts.Program, args: ParsedArgs ) => { const [watcher, initialFiles] = await new Promise< @@ -345,7 +363,13 @@ export const makeTsGdProject = async ( }) }) - return new TsGdProject(watcher, initialFiles, program, ts2gdJson, args) + return new TsGdProject({ + watcher, + initialFilePaths: initialFiles, + program, + ts2gdJson, + args, + }) } export default TsGdProject diff --git a/tests/stubs.ts b/tests/stubs.ts deleted file mode 100644 index c9f26eb1..00000000 --- a/tests/stubs.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AssetSourceFile } from "../project/assets/asset_source_file" - -export const createStubSourceFileAsset = (name: string): AssetSourceFile => { - const sourceFileAsset: AssetSourceFile = { - exportedTsClassName: () => "", - fsPath: `${name}.ts`, - name: name, - isProjectAutoload: () => false, - resPath: `res://compiled/${name}.gd`, - gdPath: `/Users/johnfn/MyGame/compiled/${name}.gd`, - tsRelativePath: "", - isAutoload: () => false, - gdContainingDirectory: "/Users/johnfn/MyGame/compiled/", - destroy: () => {}, - project: {} as any, - tsType: () => "", - compile: async () => {}, - reload: () => {}, - ...({} as any), - } - - return sourceFileAsset -} diff --git a/tests/test.ts b/tests/test.ts index 05f4323a..7f28b215 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -1,20 +1,55 @@ import fs from "fs" import path from "path" -import * as ts from "typescript" +import ts from "typescript" import * as utils from "tsutils" import chalk from "chalk" import { ParseNodeType, parseNode } from "../parse_node" import { Scope } from "../scope" -import { TsGdError, __getErrorsTestOnly } from "../errors" import { baseContentForTests } from "../generate_library_defs/generate_base" +import { ParsedArgs } from "../parse_args" +import TsGdProject, { TsGdError, Errors } from "../project" import { Paths } from "../project/paths" +import { AssetSourceFile } from "../project/assets/asset_source_file" -import { createStubSourceFileAsset } from "./stubs" +import { mockProjectPath } from "./test_utils" -export const compileTs = (code: string, isAutoload: boolean): ParseNodeType => { - const filename = isAutoload ? "autoload.ts" : "Test.ts" +export type Test = { + expected: + | string + | { type: "error"; error: string } + | { + type: "multiple-files" + files: { fileName: string; expected: string }[] + } + ts: string + fileName?: string + isAutoload?: boolean + only?: boolean + expectFail?: boolean +} + +type TestResult = TestResultPass | TestResultFail + +type TestResultPass = { type: "success" } +type TestResultFail = { + type: "fail" | "fail-error" | "fail-no-error" + fileName?: string + result: string + name: string + expected: string + expectFail?: boolean + path: string + logs?: any[][] +} + +export function compileTs( + code: string, + isAutoload = false +): [ParseNodeType, TsGdError[]] { + const filename = mockProjectPath(isAutoload ? "Autoload.ts" : "Test.ts") + const normalizedFilename = path.normalize(filename) const sourceFile = ts.createSourceFile( filename, @@ -40,7 +75,8 @@ export const compileTs = (code: string, isAutoload: boolean): ParseNodeType => { const customCompilerHost: ts.CompilerHost = { getSourceFile: (name, languageVersion) => { - if (name === filename) { + const normalizedName = path.normalize(name) + if (normalizedName === normalizedFilename) { return sourceFile } else if (name === "lib.d.ts") { return libDTs @@ -63,124 +99,49 @@ export const compileTs = (code: string, isAutoload: boolean): ParseNodeType => { } const program = ts.createProgram( - ["Test.ts", "autoload.ts"], + [mockProjectPath("Test.ts"), mockProjectPath("Autoload.ts")], tsconfigOptions, customCompilerHost ) - const sourceFileAsset = createStubSourceFileAsset("Test") + const args: ParsedArgs = { + buildLibraries: false, + buildOnly: false, + printVersion: false, + debug: false, + help: false, + init: false, + tsgdPath: mockProjectPath("ts2gd.json"), + } + + const project = new TsGdProject({ + program, + args, + initialFilePaths: [ + mockProjectPath("project.godot"), + mockProjectPath("main.tscn"), + mockProjectPath("Test.ts"), + mockProjectPath("Autoload.ts"), + ], + ts2gdJson: new Paths(args), + }) + + const sourceFileAsset = new AssetSourceFile(filename, project) - // TODO: Make this less silly. - // I suppose we could actually use the example project const godotFile = parseNode(sourceFile, { indent: "", sourceFile: sourceFile, scope: new Scope(program), isConstructor: false, program, - project: { - args: { - buildLibraries: false, - buildOnly: false, - printVersion: false, - debug: false, - help: false, - init: false, - }, - buildDynamicDefinitions: async () => {}, - assets: [], - program: undefined as any, - compileAllSourceFiles: async () => true, - shouldBuildLibraryDefinitions: () => false, - validateAutoloads: () => [], - buildLibraryDefinitions: async () => {}, - paths: {} as any, - definitionBuilder: {} as any, - mainScene: { - fsPath: "", - resPath: "", - nodes: [], - resources: [], - name: "mainScene", - project: {} as any, - rootNode: {} as any, - } as any, - godotScenes: () => [], - createAsset: () => 0 as any, - godotFonts: () => [], - godotImages: () => [], - godotGlbs: () => [], - godotProject: { - fsPath: "", - autoloads: [{ resPath: "autoload.ts" }], - mainScene: {} as any, - rawConfig: 0 as any, - actionNames: [], - project: {} as any, - addAutoload: {} as any, - removeAutoload: {} as any, - }, - monitor: () => 0 as any, - onAddAsset: async () => "", - onChangeAsset: async () => "", - onRemoveAsset: async () => {}, - sourceFiles: () => [ - { - exportedTsClassName: () => "", - fsPath: "autoload.ts", - isProjectAutoload: () => true, - isAutoload: () => true, - resPath: "", - tsRelativePath: "", - gdContainingDirectory: "", - destroy: () => {}, - project: {} as any, - tsType: () => "", - compile: async () => {}, - gdPath: "", - reload: () => {}, - isDecoratedAutoload: {} as any, - ...({} as any), // ssh about private properties. - }, - sourceFileAsset, - ], - }, + project, sourceFileAsset: sourceFileAsset, mostRecentControlStructureIsSwitch: false, - isAutoload: false, + isAutoload, usages: utils.collectVariableUsage(sourceFile), }) - return godotFile -} - -export type Test = { - expected: - | string - | { type: "error"; error: string } - | { - type: "multiple-files" - files: { fileName: string; expected: string }[] - } - ts: string - fileName?: string - isAutoload?: boolean - only?: boolean - expectFail?: boolean -} - -type TestResult = TestResultPass | TestResultFail - -type TestResultPass = { type: "success" } -type TestResultFail = { - type: "fail" | "fail-error" | "fail-no-error" - fileName?: string - result: string - name: string - expected: string - expectFail?: boolean - path: string - logs?: any[][] + return [godotFile, project.errors.get()] } const trim = (s: string) => { @@ -221,9 +182,9 @@ const test = ( let errors: TsGdError[] = [] try { - compiled = compileTs(ts, props.isAutoload ?? false) - - errors = __getErrorsTestOnly() + let tuple = compileTs(ts, props.isAutoload) + compiled = tuple[0] + errors = tuple[1] } catch (e) { return { type: "fail", diff --git a/tests/test_utils.ts b/tests/test_utils.ts new file mode 100644 index 00000000..3d3ecd58 --- /dev/null +++ b/tests/test_utils.ts @@ -0,0 +1,5 @@ +import path from "path" + +export function mockProjectPath(...segments: string[]): string { + return path.join(process.cwd(), "mockProject/", ...segments) +} diff --git a/ts_utils.ts b/ts_utils.ts index 2aebab9c..20754976 100644 --- a/ts_utils.ts +++ b/ts_utils.ts @@ -5,7 +5,7 @@ import chalk from "chalk" import ts, { ObjectFlags, SyntaxKind, TypeFlags } from "typescript" import { ParseState } from "./parse_node" -import { ErrorName, addError } from "./errors" +import { ErrorName } from "./project" export const isNullableNode = (node: ts.Node, typechecker: ts.TypeChecker) => { const type = typechecker.getTypeAtLocation(node) @@ -200,7 +200,7 @@ with either "int" or "float".` )} with either "int" or "float".` } - addError({ + props.project.errors.add({ description: errorString, error: ErrorName.InvalidNumber, location: node, @@ -246,7 +246,7 @@ with either "int" or "float".` // For exports, we really want to do a best effort to get *a* typename if (!actualType) { - addError({ + props.project.errors.add({ description: `This exported variable needs a type declaration: ${chalk.yellow(node.getText())} @@ -296,7 +296,7 @@ ${chalk.yellow(node.getText())} } if (nonNullTypes.length > 1 || nonNullTypeNodes.length > 1) { - addError({ + props.project.errors.add({ description: `You can't export a union type: ${chalk.yellow(node.getText())} diff --git a/tsconfig.json b/tsconfig.json index 2c4f8641..e151c69b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,7 @@ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ + "skipLibCheck": true, // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */