diff --git a/goldens/ts/flatbuffers/goldens/universe.ts b/goldens/ts/flatbuffers/goldens/universe.ts index 5634d78314a..acae67b69b4 100644 --- a/goldens/ts/flatbuffers/goldens/universe.ts +++ b/goldens/ts/flatbuffers/goldens/universe.ts @@ -4,7 +4,7 @@ import * as flatbuffers from 'flatbuffers'; -import { Galaxy } from '../../flatbuffers/goldens/galaxy.js'; +import { Galaxy } from './galaxy.js'; export class Universe { diff --git a/src/idl_gen_ts.cpp b/src/idl_gen_ts.cpp index aa05c23ab8f..83f5af67ba8 100644 --- a/src/idl_gen_ts.cpp +++ b/src/idl_gen_ts.cpp @@ -318,9 +318,10 @@ class TsGenerator : public BaseGenerator { export_counter++; } - if (export_counter > 0) + if (export_counter > 0) { parser_.opts.file_saver->SaveFile(it.second.filepath.c_str(), code, false); + } } } @@ -641,7 +642,8 @@ class TsGenerator : public BaseGenerator { } void GenStructArgs(import_set& imports, const StructDef& struct_def, - std::string* arguments, const std::string& nameprefix) { + const Definition& owner, std::string* arguments, + const std::string& nameprefix) { for (auto it = struct_def.fields.vec.begin(); it != struct_def.fields.vec.end(); ++it) { auto& field = **it; @@ -649,11 +651,11 @@ class TsGenerator : public BaseGenerator { // Generate arguments for a struct inside a struct. To ensure names // don't clash, and to make it obvious these arguments are constructing // a nested struct, prefix the name with the field name. - GenStructArgs(imports, *field.value.type.struct_def, arguments, + GenStructArgs(imports, *field.value.type.struct_def, owner, arguments, nameprefix + field.name + "_"); } else { *arguments += ", " + nameprefix + field.name + ": " + - GenTypeName(imports, field, field.value.type, true, + GenTypeName(imports, owner, field.value.type, true, field.IsOptional()); } } @@ -915,6 +917,48 @@ class TsGenerator : public BaseGenerator { return symbols_expression; } + std::vector PathComponents(const std::string& path) const { + std::vector components; + size_t start = 0; + while (start < path.size()) { + auto end = path.find(kPathSeparator, start); + if (end == std::string::npos) end = path.size(); + if (end > start) { + components.emplace_back(path.substr(start, end - start)); + } + if (end == path.size()) break; + start = end + 1; + } + return components; + } + + std::string RelativeDirectory(const std::vector& from, + const std::vector& to) const { + size_t common = 0; + while (common < from.size() && common < to.size() && + from[common] == to[common]) { + ++common; + } + + std::string rel; + const size_t ups = from.size() - common; + if (ups == 0) { + rel = "."; + } else { + for (size_t i = 0; i < ups; ++i) { + if (!rel.empty()) rel += kPathSeparator; + rel += ".."; + } + } + + for (size_t i = common; i < to.size(); ++i) { + if (!rel.empty()) rel += kPathSeparator; + rel += to[i]; + } + + return rel; + } + template ImportDefinition AddImport(import_set& imports, const Definition& dependent, const DefinitionT& dependency) { @@ -942,26 +986,32 @@ class TsGenerator : public BaseGenerator { const std::string symbols_expression = GenSymbolExpression( dependency, has_name_clash, import_name, name, object_name); - std::string bare_file_path; - std::string rel_file_path; - if (dependent.defined_namespace) { - const auto& dep_comps = dependent.defined_namespace->components; - for (size_t i = 0; i < dep_comps.size(); i++) { - rel_file_path += i == 0 ? ".." : (kPathSeparator + std::string("..")); - } - if (dep_comps.size() == 0) { - rel_file_path += "."; - } - } else { - rel_file_path += ".."; - } + const Namespace* dependent_ns = dependent.defined_namespace + ? dependent.defined_namespace + : parser_.empty_namespace_; + const Namespace* dependency_ns = dependency.defined_namespace + ? dependency.defined_namespace + : parser_.empty_namespace_; - bare_file_path += - kPathSeparator + - namer_.Directories(dependency.defined_namespace->components, - SkipDir::OutputPath) + + const std::string dependent_dirs = + namer_.Directories(*dependent_ns, SkipDir::OutputPath); + const std::string dependency_dirs = + namer_.Directories(*dependency_ns, SkipDir::OutputPath); + + const auto dependent_components = PathComponents(dependent_dirs); + const auto dependency_components = PathComponents(dependency_dirs); + + std::string rel_dir = + RelativeDirectory(dependent_components, dependency_components); + if (rel_dir.empty()) rel_dir = "."; + if (!rel_dir.empty()) rel_dir += kPathSeparator; + + std::string rel_file_path = + rel_dir + namer_.File(dependency, SkipFile::SuffixAndExtension); + + std::string bare_file_path = + kPathSeparator + dependency_dirs + namer_.File(dependency, SkipFile::SuffixAndExtension); - rel_file_path += bare_file_path; ImportDefinition import; import.name = name; @@ -1624,11 +1674,12 @@ class TsGenerator : public BaseGenerator { GenDocComment(struct_def.doc_comment, code_ptr); code += "export class "; code += object_name; - if (parser.opts.generate_object_based_api) + if (parser.opts.generate_object_based_api) { code += " implements flatbuffers.IUnpackableObject<" + object_api_name + "> {\n"; - else + } else { code += " {\n"; + } code += " bb: flatbuffers.ByteBuffer|null = null;\n"; code += " bb_pos = 0;\n"; @@ -2043,7 +2094,7 @@ class TsGenerator : public BaseGenerator { // Emit a factory constructor if (struct_def.fixed) { std::string arguments; - GenStructArgs(imports, struct_def, &arguments, ""); + GenStructArgs(imports, struct_def, struct_def, &arguments, ""); GenDocComment(code_ptr); code += "static create" + GetPrefixedName(struct_def) + diff --git a/tests/ts/JavaScriptRelativeImportPathTest.js b/tests/ts/JavaScriptRelativeImportPathTest.js new file mode 100644 index 00000000000..283f687479c --- /dev/null +++ b/tests/ts/JavaScriptRelativeImportPathTest.js @@ -0,0 +1,28 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const headerPath = resolve(here, "relative_imports/transit/three/header.ts"); + +const contents = readFileSync(headerPath, "utf8"); + +const expectedImports = [ + "from '../one/info.js';", + "from '../two/identity.js';", +]; + +for (const expected of expectedImports) { + if (!contents.includes(expected)) { + throw new Error(`Missing relative import "${expected}" in ${headerPath}`); + } +} + +const forbidden = "../transit/"; +if (contents.includes(forbidden)) { + throw new Error( + `Found unexpected namespace segment in import path within ${headerPath}` + ); +} + +console.log("JavaScriptRelativeImportPathTest: OK"); diff --git a/tests/ts/TypeScriptTest.py b/tests/ts/TypeScriptTest.py index a12bc25ab5b..59ee44bb497 100755 --- a/tests/ts/TypeScriptTest.py +++ b/tests/ts/TypeScriptTest.py @@ -185,6 +185,20 @@ def esbuild(input, output): flatc(options=["--ts"], schema="../long_namespace.fbs") flatc(options=["--ts"], schema="../longer_namespace.fbs") + +flatc( + options=[ + "--ts", + "--reflect-names", + "--gen-name-strings", + "--gen-object-api", + "--ts-entry-points", + "--ts-flat-files", + ], + schema="relative_imports/relative_imports.fbs", + prefix="relative_imports", +) + print("Running TypeScript Compiler...") check_call(["tsc"]) print( @@ -201,6 +215,7 @@ def esbuild(input, output): check_call(NODE_CMD + ["JavaScriptFlexBuffersTest"]) check_call(NODE_CMD + ["JavaScriptComplexArraysTest"]) check_call(NODE_CMD + ["JavaScriptUnionUnderlyingTypeTest"]) +check_call(NODE_CMD + ["JavaScriptRelativeImportPathTest"]) print("Running old v1 TypeScript Tests...") check_call(NODE_CMD + ["JavaScriptTestv1.cjs", "./monster_test_generated.cjs"]) diff --git a/tests/ts/my-game/example/any-ambiguous-aliases.ts b/tests/ts/my-game/example/any-ambiguous-aliases.ts index 8e7a3a3b653..7510e3056c0 100644 --- a/tests/ts/my-game/example/any-ambiguous-aliases.ts +++ b/tests/ts/my-game/example/any-ambiguous-aliases.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ -import { Monster, MonsterT } from '../../my-game/example/monster.js'; +import { Monster, MonsterT } from './monster.js'; export enum AnyAmbiguousAliases { diff --git a/tests/ts/my-game/example/any-unique-aliases.ts b/tests/ts/my-game/example/any-unique-aliases.ts index ae85ea00b92..c0b7e15c7f1 100644 --- a/tests/ts/my-game/example/any-unique-aliases.ts +++ b/tests/ts/my-game/example/any-unique-aliases.ts @@ -2,9 +2,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ -import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../../my-game/example2/monster.js'; -import { Monster, MonsterT } from '../../my-game/example/monster.js'; -import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from '../../my-game/example/test-simple-table-with-enum.js'; +import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../example2/monster.js'; +import { Monster, MonsterT } from './monster.js'; +import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from './test-simple-table-with-enum.js'; export enum AnyUniqueAliases { diff --git a/tests/ts/my-game/example/any.ts b/tests/ts/my-game/example/any.ts index 5e484fa0afa..ea886c787e5 100644 --- a/tests/ts/my-game/example/any.ts +++ b/tests/ts/my-game/example/any.ts @@ -2,9 +2,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ -import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../../my-game/example2/monster.js'; -import { Monster, MonsterT } from '../../my-game/example/monster.js'; -import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from '../../my-game/example/test-simple-table-with-enum.js'; +import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../example2/monster.js'; +import { Monster, MonsterT } from './monster.js'; +import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from './test-simple-table-with-enum.js'; export enum Any { diff --git a/tests/ts/my-game/example/monster.ts b/tests/ts/my-game/example/monster.ts index bd64dd1210c..04ca1ac73a8 100644 --- a/tests/ts/my-game/example/monster.ts +++ b/tests/ts/my-game/example/monster.ts @@ -4,19 +4,19 @@ import * as flatbuffers from 'flatbuffers'; -import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../../my-game/example2/monster.js'; -import { Ability, AbilityT } from '../../my-game/example/ability.js'; -import { Any, unionToAny, unionListToAny } from '../../my-game/example/any.js'; -import { AnyAmbiguousAliases, unionToAnyAmbiguousAliases, unionListToAnyAmbiguousAliases } from '../../my-game/example/any-ambiguous-aliases.js'; -import { AnyUniqueAliases, unionToAnyUniqueAliases, unionListToAnyUniqueAliases } from '../../my-game/example/any-unique-aliases.js'; -import { Color } from '../../my-game/example/color.js'; -import { Race } from '../../my-game/example/race.js'; -import { Referrable, ReferrableT } from '../../my-game/example/referrable.js'; -import { Stat, StatT } from '../../my-game/example/stat.js'; -import { Test, TestT } from '../../my-game/example/test.js'; -import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from '../../my-game/example/test-simple-table-with-enum.js'; -import { Vec3, Vec3T } from '../../my-game/example/vec3.js'; -import { InParentNamespace, InParentNamespaceT } from '../../my-game/in-parent-namespace.js'; +import { Monster as MyGame_Example2_Monster, MonsterT as MyGame_Example2_MonsterT } from '../example2/monster.js'; +import { Ability, AbilityT } from './ability.js'; +import { Any, unionToAny, unionListToAny } from './any.js'; +import { AnyAmbiguousAliases, unionToAnyAmbiguousAliases, unionListToAnyAmbiguousAliases } from './any-ambiguous-aliases.js'; +import { AnyUniqueAliases, unionToAnyUniqueAliases, unionListToAnyUniqueAliases } from './any-unique-aliases.js'; +import { Color } from './color.js'; +import { Race } from './race.js'; +import { Referrable, ReferrableT } from './referrable.js'; +import { Stat, StatT } from './stat.js'; +import { Test, TestT } from './test.js'; +import { TestSimpleTableWithEnum, TestSimpleTableWithEnumT } from './test-simple-table-with-enum.js'; +import { Vec3, Vec3T } from './vec3.js'; +import { InParentNamespace, InParentNamespaceT } from '../in-parent-namespace.js'; /** diff --git a/tests/ts/my-game/example/struct-of-structs-of-structs.ts b/tests/ts/my-game/example/struct-of-structs-of-structs.ts index af3519a0168..f47d42d248f 100644 --- a/tests/ts/my-game/example/struct-of-structs-of-structs.ts +++ b/tests/ts/my-game/example/struct-of-structs-of-structs.ts @@ -4,7 +4,7 @@ import * as flatbuffers from 'flatbuffers'; -import { StructOfStructs, StructOfStructsT } from '../../my-game/example/struct-of-structs.js'; +import { StructOfStructs, StructOfStructsT } from './struct-of-structs.js'; export class StructOfStructsOfStructs implements flatbuffers.IUnpackableObject { diff --git a/tests/ts/my-game/example/struct-of-structs.ts b/tests/ts/my-game/example/struct-of-structs.ts index 2f4a36748bc..c3ff69da39a 100644 --- a/tests/ts/my-game/example/struct-of-structs.ts +++ b/tests/ts/my-game/example/struct-of-structs.ts @@ -4,8 +4,8 @@ import * as flatbuffers from 'flatbuffers'; -import { Ability, AbilityT } from '../../my-game/example/ability.js'; -import { Test, TestT } from '../../my-game/example/test.js'; +import { Ability, AbilityT } from './ability.js'; +import { Test, TestT } from './test.js'; export class StructOfStructs implements flatbuffers.IUnpackableObject { diff --git a/tests/ts/my-game/example/test-simple-table-with-enum.ts b/tests/ts/my-game/example/test-simple-table-with-enum.ts index 3e8b4c03fc6..ac9d7add2ae 100644 --- a/tests/ts/my-game/example/test-simple-table-with-enum.ts +++ b/tests/ts/my-game/example/test-simple-table-with-enum.ts @@ -4,7 +4,7 @@ import * as flatbuffers from 'flatbuffers'; -import { Color } from '../../my-game/example/color.js'; +import { Color } from './color.js'; export class TestSimpleTableWithEnum implements flatbuffers.IUnpackableObject { diff --git a/tests/ts/my-game/example/vec3.ts b/tests/ts/my-game/example/vec3.ts index a2092d700e8..a5fd359fe7c 100644 --- a/tests/ts/my-game/example/vec3.ts +++ b/tests/ts/my-game/example/vec3.ts @@ -4,8 +4,8 @@ import * as flatbuffers from 'flatbuffers'; -import { Color } from '../../my-game/example/color.js'; -import { Test, TestT } from '../../my-game/example/test.js'; +import { Color } from './color.js'; +import { Test, TestT } from './test.js'; export class Vec3 implements flatbuffers.IUnpackableObject { diff --git a/tests/ts/optional-scalars/scalar-stuff.ts b/tests/ts/optional-scalars/scalar-stuff.ts index 7f6fdcaae2c..69961af263e 100644 --- a/tests/ts/optional-scalars/scalar-stuff.ts +++ b/tests/ts/optional-scalars/scalar-stuff.ts @@ -4,7 +4,7 @@ import * as flatbuffers from 'flatbuffers'; -import { OptionalByte } from '../optional-scalars/optional-byte.js'; +import { OptionalByte } from './optional-byte.js'; export class ScalarStuff { diff --git a/tests/ts/relative_imports/relative_imports.fbs b/tests/ts/relative_imports/relative_imports.fbs new file mode 100644 index 00000000000..b599a1b5595 --- /dev/null +++ b/tests/ts/relative_imports/relative_imports.fbs @@ -0,0 +1,20 @@ +namespace Transit.One; + +table Info { + timestamp:ulong; +} + +namespace Transit.Two; + +table Identity { + id:uint; +} + +namespace Transit.Three; + +table Header { + info:Transit.One.Info; + id:Transit.Two.Identity; +} + +root_type Header; diff --git a/tests/ts/tsconfig.json b/tests/ts/tsconfig.json index 62a31c438f4..113fb37676f 100644 --- a/tests/ts/tsconfig.json +++ b/tests/ts/tsconfig.json @@ -1,7 +1,10 @@ { "compilerOptions": { "target": "ES2020", - "lib": ["ES2020", "DOM"], + "lib": [ + "ES2020", + "DOM" + ], "module": "NodeNext", "declaration": true, "strict": true @@ -17,6 +20,7 @@ "arrays_test_complex/**/*.ts", "union_underlying_type_test.ts", "long-namespace/**/*.ts", - "longer-namespace/**/*.ts" + "longer-namespace/**/*.ts", + "relative_imports/**/*.ts" ] -} +} \ No newline at end of file