diff options
author | khoden <khoden@yandex-team.com> | 2023-09-13 11:12:22 +0300 |
---|---|---|
committer | khoden <khoden@yandex-team.com> | 2023-09-13 11:45:12 +0300 |
commit | 28666be3eead21a7ab832b3a9babaf95cad841d3 (patch) | |
tree | 8fb0113f700b92c6f34dcb68dab46c448264c1f5 | |
parent | 9747bd43f856cf97bec52a3b55057c0907c6dc28 (diff) | |
download | ydb-28666be3eead21a7ab832b3a9babaf95cad841d3.tar.gz |
nots/plugins: Проблемы c `extends` у `tsconfig.json`
~~В этот PR только простой фикс конкретного бага из https://a.yandex-team.ru/review/4370819/details~~
~~Прочие улучшения в коде `ts_config.py` будут отдельно, включая покрытие кода тестами.~~
Переписал `TsConfig.merge()`, чуть добавил тестов
-rw-r--r-- | build/conf/ts/ts.conf | 4 | ||||
-rw-r--r-- | build/plugins/lib/nots/typescript/tests/test_ts_config.py | 169 | ||||
-rw-r--r-- | build/plugins/lib/nots/typescript/tests/ts_config.py | 86 | ||||
-rw-r--r-- | build/plugins/lib/nots/typescript/tests/ya.make | 2 | ||||
-rw-r--r-- | build/plugins/lib/nots/typescript/ts_config.py | 158 |
5 files changed, 242 insertions, 177 deletions
diff --git a/build/conf/ts/ts.conf b/build/conf/ts/ts.conf index dbf05d48b0..942b21c274 100644 --- a/build/conf/ts/ts.conf +++ b/build/conf/ts/ts.conf @@ -57,8 +57,8 @@ TS_GLOB_INCLUDE=**/* # Hardcoded "exclude" list (reasonable default). TS_GLOB_EXCLUDE=$TS_CONFIG_PATH \ ya.make a.yaml \ - (.code|.idea)/**/* \ - (build|dist|bundle|.*|$TS_NEXT_OUTPUT_DIR)/**/* \ + (.vscode|.idea)/**/* \ + (build|dist|bundle|$WEBPACK_OUTPUT_DIR|$TS_NEXT_OUTPUT_DIR|$VITE_OUTPUT_DIR)/**/* \ node_modules/**/* package.json pnpm-lock.yaml .* \ tests/**/* **/*.(test|spec).(ts|tsx|js|jsx) diff --git a/build/plugins/lib/nots/typescript/tests/test_ts_config.py b/build/plugins/lib/nots/typescript/tests/test_ts_config.py new file mode 100644 index 0000000000..9cd3a7a184 --- /dev/null +++ b/build/plugins/lib/nots/typescript/tests/test_ts_config.py @@ -0,0 +1,169 @@ +import pytest + +from build.plugins.lib.nots.typescript import TsConfig, TsValidationError + + +def test_ts_config_validate_valid(): + cfg = TsConfig(path="/tsconfig.json") + cfg.data = { + "compilerOptions": { + "rootDir": "./src", + "outDir": "./build", + }, + } + + cfg.validate() + + +def test_ts_config_validate_empty(): + cfg = TsConfig(path="/tsconfig.json") + + with pytest.raises(TsValidationError) as e: + cfg.validate() + + assert e.value.errors == [ + "'rootDir' option is required", + "'outDir' option is required", + ] + + +def test_ts_config_validate_invalid_common(): + cfg = TsConfig(path="/tsconfig.json") + cfg.data = { + "compilerOptions": { + "preserveSymlinks": True, + "rootDirs": [], + "outFile": "./foo.js", + }, + "references": [], + "files": [], + "include": [], + "exclude": [], + } + + with pytest.raises(TsValidationError) as e: + cfg.validate() + + assert e.value.errors == [ + "'rootDir' option is required", + "'outDir' option is required", + "'outFile' option is not supported", + "'preserveSymlinks' option is not supported due to pnpm limitations", + "'rootDirs' option is not supported, relative imports should have single root", + "'files' option is not supported, use 'include'", + "composite builds are not supported, use peerdirs in ya.make instead of 'references' option", + ] + + +def test_ts_config_validate_invalid_subdirs(): + cfg = TsConfig(path="/foo/tsconfig.json") + cfg.data = { + "compilerOptions": { + "rootDir": "/bar/src", + "outDir": "../bar/build", + }, + } + + with pytest.raises(TsValidationError) as e: + cfg.validate() + + assert e.value.errors == [ + "'outDir' should be a subdirectory of the module", + ] + + +def test_ts_config_compiler_options(): + cfg = TsConfig(path="/tsconfig.json") + + assert cfg.compiler_option("invalid") is None + + cfg.data = { + "compilerOptions": { + "rootDir": "src", + }, + } + + assert cfg.compiler_option("rootDir") == "src" + + +class TestTsConfigMerge: + def test_merge_paths(self): + # arrange + cfg_main = TsConfig(path="/foo/tsconfig.json") + cfg_main.data = {"compilerOptions": {"paths": {"path1": ["src/path1"], "path2": ["src/path2"]}}} + + cfg_common = TsConfig(path="/foo/tsconfig.common.json") + cfg_common.data = { + "compilerOptions": {"paths": {"path0": ["src/path0"]}}, + } + + # act + cfg_main.merge(".", cfg_common) + + # assert + assert cfg_main.data == { + "compilerOptions": {"paths": {"path1": ["src/path1"], "path2": ["src/path2"]}}, + } + + def test_create_compiler_options(self): + # arrange + cfg_main = TsConfig(path="/foo/tsconfig.json") + cfg_main.data = {} + + cfg_common = TsConfig(path="/foo/config/tsconfig.common.json") + cfg_common.data = { + "compilerOptions": { + "moduleResolution": "node", + }, + } + + # act + cfg_main.merge("config", cfg_common) + + # assert + assert cfg_main.data == { + "compilerOptions": { + "moduleResolution": "node", + }, + } + + def test_merge_compiler_options(self): + # arrange + cfg_main = TsConfig(path="/foo/tsconfig.json") + cfg_main.data = { + "compilerOptions": { + "esModuleInterop": True, + "moduleResolution": "nodenext", + "rootDir": "./src", + }, + "extraField1": False, + "sameField": False, + } + + cfg_common = TsConfig(path="/foo/config/tsconfig.common.json") + cfg_common.data = { + "compilerOptions": { + "moduleResolution": "node", + "outDir": "./out", + "strict": True, + }, + "extraField2": True, + "sameField": True, + } + + # act + cfg_main.merge("config", cfg_common) + + # assert + assert cfg_main.data == { + "compilerOptions": { + "esModuleInterop": True, # own value + "moduleResolution": "nodenext", # replaced value + "outDir": "config/out", # resolved path + "rootDir": "./src", # own path value (untouched) + "strict": True, # inherited value + }, + "extraField1": False, # own root field + "extraField2": True, # inherited root field + "sameField": False, # prefer own value + } diff --git a/build/plugins/lib/nots/typescript/tests/ts_config.py b/build/plugins/lib/nots/typescript/tests/ts_config.py deleted file mode 100644 index 4b8fd675b3..0000000000 --- a/build/plugins/lib/nots/typescript/tests/ts_config.py +++ /dev/null @@ -1,86 +0,0 @@ -import pytest - -from build.plugins.lib.nots.typescript import TsConfig, TsValidationError - - -def test_ts_config_validate_valid(): - cfg = TsConfig(path="/tsconfig.json") - cfg.data = { - "compilerOptions": { - "rootDir": "./src", - "outDir": "./build", - }, - } - - cfg.validate() - - -def test_ts_config_validate_empty(): - cfg = TsConfig(path="/tsconfig.json") - - with pytest.raises(TsValidationError) as e: - cfg.validate() - - assert e.value.errors == [ - "'rootDir' option is required", - "'outDir' option is required", - ] - - -def test_ts_config_validate_invalid_common(): - cfg = TsConfig(path="/tsconfig.json") - cfg.data = { - "compilerOptions": { - "preserveSymlinks": True, - "rootDirs": [], - "outFile": "./foo.js", - }, - "references": [], - "files": [], - "include": [], - "exclude": [], - } - - with pytest.raises(TsValidationError) as e: - cfg.validate() - - assert e.value.errors == [ - "'rootDir' option is required", - "'outDir' option is required", - "'outFile' option is not supported", - "'preserveSymlinks' option is not supported due to pnpm limitations", - "'rootDirs' option is not supported, relative imports should have single root", - "'files' option is not supported, use 'include'", - "composite builds are not supported, use peerdirs in ya.make instead of 'references' option", - ] - - -def test_ts_config_validate_invalid_subdirs(): - cfg = TsConfig(path="/foo/tsconfig.json") - cfg.data = { - "compilerOptions": { - "rootDir": "/bar/src", - "outDir": "../bar/build", - }, - } - - with pytest.raises(TsValidationError) as e: - cfg.validate() - - assert e.value.errors == [ - "'outDir' should be a subdirectory of the module", - ] - - -def test_ts_config_compiler_options(): - cfg = TsConfig(path="/tsconfig.json") - - assert cfg.compiler_option("invalid") is None - - cfg.data = { - "compilerOptions": { - "rootDir": "src", - }, - } - - assert cfg.compiler_option("rootDir") == "src" diff --git a/build/plugins/lib/nots/typescript/tests/ya.make b/build/plugins/lib/nots/typescript/tests/ya.make index 2e038f6c96..20fc215686 100644 --- a/build/plugins/lib/nots/typescript/tests/ya.make +++ b/build/plugins/lib/nots/typescript/tests/ya.make @@ -3,7 +3,7 @@ PY23_TEST() OWNER(g:frontend-build-platform) TEST_SRCS( - ts_config.py + test_ts_config.py test_ts_glob.py ) diff --git a/build/plugins/lib/nots/typescript/ts_config.py b/build/plugins/lib/nots/typescript/ts_config.py index b4ad9c3d3f..afe7578013 100644 --- a/build/plugins/lib/nots/typescript/ts_config.py +++ b/build/plugins/lib/nots/typescript/ts_config.py @@ -9,21 +9,32 @@ from ..package_manager.base import utils DEFAULT_TS_CONFIG_FILE = "tsconfig.json" -def merge_dicts(d1, d2): - """ - Merges two dicts recursively assuming that both have similar structure. - If d1.x.y.z has different type than d2.x.y.z then d2 will override d1 and result value res.x.y.z == d2.x.y.z. - If corresponding values are lists then the result will have a sum of those lists. - """ - if isinstance(d1, dict) and isinstance(d2, dict): - for k in d2: - d1[k] = merge_dicts(d1[k], d2[k]) if k in d1 else d2[k] - else: - if isinstance(d1, list) and isinstance(d2, list): - return d1 + d2 - else: - return d2 - return d1 +class RootFields: + extends = 'extends' + + exclude = 'exclude' + files = 'files' + include = 'include' + + compilerOptions = 'compilerOptions' + + PATH_LIST_FIELDS = { + exclude, + files, + include, + } + + +class CompilerOptionsFields: + baseUrl = 'baseUrl' + outDir = 'outDir' + rootDir = 'rootDir' + + PATH_FIELDS = { + baseUrl, + outDir, + rootDir, + } class TsConfig(object): @@ -54,52 +65,46 @@ class TsConfig(object): raise TsError("Failed to read tsconfig {}: {}".format(self.path, e)) def merge(self, rel_path, base_tsconfig): + # type: (TsConfig, str, TsConfig) -> None """ :param rel_path: relative path to the configuration file we are merging in. It is required to set the relative paths correctly. - :type rel_path: str + :param base_tsconfig: base TsConfig we are merging with our TsConfig instance - :type base_tsconfig: dict """ if not base_tsconfig.data: return + # 'data' from the file in 'extends' + base_data = copy.deepcopy(base_tsconfig.data) + def relative_path(p): return os.path.normpath(os.path.join(rel_path, p)) - base_config_data = copy.deepcopy(base_tsconfig.data) - - parameter_section_labels = ["compilerOptions", "typeAcquisition", "watchOptions"] - for opt_label in parameter_section_labels: - base_options = base_config_data.get(opt_label) - if not base_options: - continue - - new_options = self.data.get(opt_label) - for key in base_options: - val = base_options[key] - - # lists of paths - if key in ["extends", "outDir", "rootDir", "baseUrl", "include"]: - val = relative_path(val) - - # path string - elif key in ["rootDirs", "excludeDirectories", "excludeFiles"]: - val = map(relative_path, val) + for root_field, root_value in base_data.items(): + # extends + if root_field == RootFields.extends: + # replace itself to its own `extends` (for multi level extends) + self.data[RootFields.extends] = relative_path(root_value) - # dicts having paths as values - elif key in ["paths"]: - new_paths = new_options.get(key) - val = map(relative_path, val) + (new_paths if new_paths else []) + # exclude, files, include + elif root_field in RootFields.PATH_LIST_FIELDS: + if root_field not in self.data: + self.data[root_field] = [relative_path(p) for p in root_value] - base_options[key] = val + # compilerOptions + elif root_field == RootFields.compilerOptions: + for option, option_value in root_value.items(): + is_path_field = option in CompilerOptionsFields.PATH_FIELDS - if new_options and base_options: - base_options.update(new_options) - self.data[opt_label] = base_options + if not self.has_compiler_option(option): + new_value = relative_path(option_value) if is_path_field else option_value + self.set_compiler_option(option, new_value) - base_config_data.update(self.data) - self.data = base_config_data + # other fields (just copy if it has not existed) + elif root_field not in self.data: + self.data[root_field] = root_value + pass def inline_extend(self, dep_paths): """ @@ -111,7 +116,7 @@ class TsConfig(object): :type dep_paths: dict :rtype: list of str """ - ext_value = self.data.get("extends") + ext_value = self.data.get(RootFields.extends) if not ext_value: return [] @@ -142,7 +147,7 @@ class TsConfig(object): paths = [base_config_path] + base_config.inline_extend(dep_paths) self.merge(rel_path, base_config) - del self.data["extends"] + del self.data[RootFields.extends] return paths @@ -151,21 +156,10 @@ class TsConfig(object): Returns ref to the "compilerOptions" dict. :rtype: dict """ - opts = self.data.get("compilerOptions") - if opts is None: - opts = {} - self.data["compilerOptions"] = opts + if RootFields.compilerOptions not in self.data: + self.data[RootFields.compilerOptions] = {} - return opts - - def prepend_include(self, value): - """ - Prepends `value` to `include` list - :param value: value to prepend - :type value: str - """ - includeList = self.data.get("include") - self.data["include"] = [value] + includeList + return self.data[RootFields.compilerOptions] def compiler_option(self, name, default=None): """ @@ -177,28 +171,16 @@ class TsConfig(object): """ return self.get_or_create_compiler_options().get(name, default) - def add_to_compiler_option(self, name, add_value): - """ - Merges the existing value with add_value for the option with label=name. - Merge is done recursively if the value is of a dict instance. - :param name: option key - :type name: str - :param value: option value to set - :type value: mixed - """ - default_value = {} if isinstance(add_value, dict) else [] - opts = self.get_or_create_compiler_options() - opts[name] = merge_dicts(opts.get(name, default_value), add_value) + def has_compiler_option(self, name): + # type: (str) -> bool + compiler_options = self.data.get(RootFields.compilerOptions, {}) - def inject_plugin(self, plugin): - """ - :param plugin: plugin dict (ts-patch compatible, see https://github.com/nonara/ts-patch) - :type plugin: dict of str - """ - opts = self.get_or_create_compiler_options() - if not opts.get("plugins"): - opts["plugins"] = [] - opts["plugins"].append(plugin) + return name in compiler_options + + def set_compiler_option(self, name, value): + # type: (str, Any) -> None + compiler_options = self.get_or_create_compiler_options() + compiler_options[name] = value def validate(self): """ @@ -206,8 +188,8 @@ class TsConfig(object): """ opts = self.get_or_create_compiler_options() errors = [] - root_dir = opts.get("rootDir") - out_dir = opts.get("outDir") + root_dir = opts.get(CompilerOptionsFields.rootDir) + out_dir = opts.get(CompilerOptionsFields.outDir) config_dir = os.path.dirname(self.path) def is_mod_subdir(p): @@ -262,9 +244,9 @@ class TsConfig(object): """ ts_glob_config = TsGlobConfig( - root_dir=self.compiler_option("rootDir"), - out_dir=self.compiler_option("outDir"), - include=self.data.get("include"), + root_dir=self.compiler_option(CompilerOptionsFields.rootDir), + out_dir=self.compiler_option(CompilerOptionsFields.outDir), + include=self.data.get(RootFields.include), ) return ts_glob(ts_glob_config, all_files) |