nots.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. import os
  2. from enum import auto, StrEnum
  3. from typing import Any, Literal, TYPE_CHECKING
  4. # noinspection PyUnresolvedReferences
  5. import ymake
  6. import _dart_fields as df
  7. import ytest
  8. from _common import (
  9. rootrel_arc_src,
  10. sort_uniq,
  11. strip_roots,
  12. to_yesno,
  13. )
  14. from _dart_fields import create_dart_record
  15. if TYPE_CHECKING:
  16. from lib.nots.erm_json_lite import ErmJsonLite
  17. from lib.nots.package_manager import PackageManagerType, BasePackageManager
  18. from lib.nots.semver import Version
  19. from lib.nots.typescript import TsConfig
  20. # 1 is 60 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  21. # 0.5 is 120 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  22. # 0.2 is 300 files per chunk for TIMEOUT(60) - default timeout for SIZE(SMALL)
  23. ESLINT_FILE_PROCESSING_TIME_DEFAULT = 0.2 # seconds per file
  24. REQUIRED_MISSING = "~~required~~"
  25. class COLORS:
  26. """
  27. See https://en.m.wikipedia.org/wiki/ANSI_escape_code#Colors for details
  28. """
  29. @staticmethod
  30. def _wrap_color(color_code: int) -> str:
  31. return f"\033[0;{color_code}m"
  32. red = _wrap_color(31)
  33. green = _wrap_color(32)
  34. yellow = _wrap_color(33)
  35. cyan = _wrap_color(36)
  36. reset = _wrap_color(49)
  37. class TsTestType(StrEnum):
  38. ESLINT = auto()
  39. HERMIONE = auto()
  40. JEST = auto()
  41. PLAYWRIGHT = auto()
  42. PLAYWRIGHT_LARGE = auto()
  43. TSC_TYPECHECK = auto()
  44. TS_STYLELINT = auto()
  45. class UnitType:
  46. MessageType = Literal["INFO", "WARN", "ERROR"]
  47. PluginArgs = str | list[str] | tuple[str]
  48. def message(self, args: list[MessageType | str]) -> None:
  49. """
  50. Print message to the log
  51. """
  52. def get(self, var_name: str) -> str | None:
  53. """
  54. Get variable value
  55. """
  56. def set(self, args: PluginArgs) -> None:
  57. """
  58. Set variable value
  59. """
  60. def enabled(self, var_name: str) -> None:
  61. """
  62. Set variable value to "yes"
  63. """
  64. def disabled(self, var_name: str) -> None:
  65. """
  66. Set variable value to "no"
  67. """
  68. def set_property(self, args: PluginArgs) -> None:
  69. """
  70. TODO (set vs set_property?)
  71. """
  72. def resolve(self, path: str) -> str:
  73. """
  74. Resolve path TODO?
  75. """
  76. def resolve_arc_path(self, path: str) -> str:
  77. """
  78. Resolve path TODO?
  79. """
  80. def path(self) -> str:
  81. """
  82. Get the project path
  83. """
  84. def ondepends(self, deps: PluginArgs) -> None:
  85. """
  86. Run DEPENDS(...)
  87. """
  88. def onpeerdir(self, args: str | list[str]) -> None:
  89. """
  90. Run PEERDIR(...)
  91. """
  92. class NotsUnitType(UnitType):
  93. def on_peerdir_ts_resource(self, *resources: str):
  94. """
  95. Ensure dependency installed on the project
  96. Also check its version (is it supported by erm)
  97. """
  98. def on_do_ts_yndexing(self) -> None:
  99. """
  100. Turn on code navigation indexing
  101. """
  102. def on_from_npm(self, args: UnitType.PluginArgs) -> None:
  103. """
  104. TODO remove after removing on_from_pnpm_lockfiles
  105. """
  106. def on_setup_install_node_modules_recipe(self) -> None:
  107. """
  108. Setup test recipe to install node_modules before running tests
  109. """
  110. def on_setup_extract_node_modules_recipe(self, args: UnitType.PluginArgs) -> None:
  111. """
  112. Setup test recipe to extract workspace-node_modules.tar before running tests
  113. """
  114. def on_setup_extract_output_tars_recipe(self, args: UnitType.PluginArgs) -> None:
  115. """
  116. Setup test recipe to extract peer's output before running tests
  117. """
  118. TS_TEST_FIELDS_BASE = (
  119. df.BinaryPath.normalized,
  120. df.BuildFolderPath.normalized,
  121. df.ForkMode.test_fork_mode,
  122. df.NodejsRootVarName.value,
  123. df.ScriptRelPath.first_flat,
  124. df.SourceFolderPath.normalized,
  125. df.SplitFactor.from_unit,
  126. df.TestData.from_unit,
  127. df.TestedProjectName.filename_without_ext,
  128. df.TestEnv.value,
  129. df.TestName.value,
  130. df.TestRecipes.value,
  131. df.TestTimeout.from_unit,
  132. )
  133. TS_TEST_SPECIFIC_FIELDS = {
  134. TsTestType.ESLINT: (
  135. df.Size.from_unit,
  136. df.TestCwd.moddir,
  137. df.Tag.from_unit,
  138. df.Requirements.from_unit,
  139. df.EslintConfigPath.value,
  140. ),
  141. TsTestType.HERMIONE: (
  142. df.Tag.from_unit_fat_external_no_retries,
  143. df.Requirements.from_unit_with_full_network,
  144. df.ConfigPath.value,
  145. df.TsTestDataDirs.value,
  146. df.TsTestDataDirsRename.value,
  147. df.TsResources.value,
  148. df.TsTestForPath.value,
  149. ),
  150. TsTestType.JEST: (
  151. df.Size.from_unit,
  152. df.Tag.from_unit,
  153. df.Requirements.from_unit,
  154. df.ConfigPath.value,
  155. df.TsTestDataDirs.value,
  156. df.TsTestDataDirsRename.value,
  157. df.TsResources.value,
  158. df.TsTestForPath.value,
  159. ),
  160. TsTestType.PLAYWRIGHT: (
  161. df.Size.from_unit,
  162. df.Tag.from_unit,
  163. df.Requirements.from_unit,
  164. df.ConfigPath.value,
  165. df.TsTestDataDirs.value,
  166. df.TsTestDataDirsRename.value,
  167. df.TsResources.value,
  168. df.TsTestForPath.value,
  169. ),
  170. TsTestType.PLAYWRIGHT_LARGE: (
  171. df.ConfigPath.value,
  172. df.Size.from_unit,
  173. df.Tag.from_unit_fat_external_no_retries,
  174. df.Requirements.from_unit_with_full_network,
  175. df.TsResources.value,
  176. df.TsTestDataDirs.value,
  177. df.TsTestDataDirsRename.value,
  178. df.TsTestForPath.value,
  179. ),
  180. TsTestType.TSC_TYPECHECK: (
  181. df.Size.from_unit,
  182. df.TestCwd.moddir,
  183. df.Tag.from_unit,
  184. df.Requirements.from_unit,
  185. ),
  186. TsTestType.TS_STYLELINT: (
  187. df.TsStylelintConfig.value,
  188. df.TestFiles.stylesheets,
  189. df.NodeModulesBundleFilename.value,
  190. ),
  191. }
  192. class PluginLogger(object):
  193. unit: UnitType = None
  194. prefix = ""
  195. def reset(self, unit: NotsUnitType | None, prefix=""):
  196. self.unit = unit
  197. self.prefix = prefix
  198. def get_state(self):
  199. return self.unit, self.prefix
  200. def _stringify_messages(self, messages: tuple[Any, ...]):
  201. parts = []
  202. for m in messages:
  203. if m is None:
  204. parts.append("None")
  205. else:
  206. parts.append(m if isinstance(m, str) else repr(m))
  207. # cyan color (code 36) for messages
  208. return f"{COLORS.green}{self.prefix}{COLORS.reset}\n{COLORS.cyan}{" ".join(parts)}{COLORS.reset}"
  209. def info(self, *messages: Any) -> None:
  210. if self.unit:
  211. self.unit.message(["INFO", self._stringify_messages(messages)])
  212. def warn(self, *messages: Any) -> None:
  213. if self.unit:
  214. self.unit.message(["WARN", self._stringify_messages(messages)])
  215. def error(self, *messages: Any) -> None:
  216. if self.unit:
  217. self.unit.message(["ERROR", self._stringify_messages(messages)])
  218. def print_vars(self, *variables: str):
  219. if self.unit:
  220. values = ["{}={}".format(v, self.unit.get(v)) for v in variables]
  221. self.info("\n".join(values))
  222. logger = PluginLogger()
  223. def _with_report_configure_error(fn):
  224. """
  225. Handle exceptions, report them as ymake configure error
  226. Also wraps plugin function like `on<macro_name>` to register `unit` in the PluginLogger
  227. """
  228. def _wrapper(*args, **kwargs):
  229. last_state = logger.get_state()
  230. unit = args[0]
  231. logger.reset(unit if unit.get("TS_LOG") == "yes" else None, fn.__name__)
  232. try:
  233. fn(*args, **kwargs)
  234. except Exception as exc:
  235. ymake.report_configure_error(str(exc))
  236. if unit.get("TS_RAISE") == "yes":
  237. raise
  238. else:
  239. unit.message(["WARN", "Configure error is reported. Add -DTS_RAISE to see actual exception"])
  240. finally:
  241. logger.reset(*last_state)
  242. return _wrapper
  243. def _build_directives(flags: list[str] | tuple[str], paths: list[str]) -> str:
  244. parts = [p for p in (flags or []) if p]
  245. parts_str = ";".join(parts)
  246. expressions = ['${{{parts}:"{path}"}}'.format(parts=parts_str, path=path) for path in paths]
  247. return " ".join(expressions)
  248. def _build_cmd_input_paths(paths: list[str] | tuple[str], hide=False, disable_include_processor=False):
  249. hide_part = "hide" if hide else ""
  250. disable_ip_part = "context=TEXT" if disable_include_processor else ""
  251. return _build_directives([hide_part, disable_ip_part, "input"], paths)
  252. def _build_cmd_output_paths(paths: list[str] | tuple[str], hide=False):
  253. hide_part = "hide" if hide else ""
  254. return _build_directives([hide_part, "output"], paths)
  255. def _create_erm_json(unit: NotsUnitType):
  256. from lib.nots.erm_json_lite import ErmJsonLite
  257. erm_packages_path = unit.get("ERM_PACKAGES_PATH")
  258. path = unit.resolve(unit.resolve_arc_path(erm_packages_path))
  259. return ErmJsonLite.load(path)
  260. def _get_pm_type(unit: NotsUnitType) -> 'PackageManagerType':
  261. resolved: PackageManagerType | None = unit.get("PM_TYPE")
  262. if not resolved:
  263. raise Exception("PM_TYPE is not set yet. Macro _SET_PACKAGE_MANAGER() should be called before.")
  264. return resolved
  265. def _get_source_path(unit: NotsUnitType) -> str:
  266. sources_path = unit.get("TS_TEST_FOR_DIR") if unit.get("TS_TEST_FOR") else unit.path()
  267. return sources_path
  268. def _create_pm(unit: NotsUnitType) -> 'BasePackageManager':
  269. from lib.nots.package_manager import get_package_manager_type
  270. sources_path = _get_source_path(unit)
  271. module_path = unit.get("TS_TEST_FOR_PATH") if unit.get("TS_TEST_FOR") else unit.get("MODDIR")
  272. # noinspection PyPep8Naming
  273. PackageManager = get_package_manager_type(_get_pm_type(unit))
  274. return PackageManager(
  275. sources_path=unit.resolve(sources_path),
  276. build_root="$B",
  277. build_path=unit.path().replace("$S", "$B", 1),
  278. nodejs_bin_path=None,
  279. script_path=None,
  280. module_path=module_path,
  281. )
  282. @_with_report_configure_error
  283. def on_set_package_manager(unit: NotsUnitType) -> None:
  284. pm_type = "pnpm" # projects without any lockfile are processed by pnpm
  285. source_path = _get_source_path(unit)
  286. for pm_key, lockfile_name in [("pnpm", "pnpm-lock.yaml"), ("npm", "package-lock.json")]:
  287. lf_path = os.path.join(source_path, lockfile_name)
  288. lf_path_resolved = unit.resolve_arc_path(strip_roots(lf_path))
  289. if lf_path_resolved:
  290. pm_type = pm_key
  291. break
  292. if pm_type == 'npm' and "devtools/dummy_arcadia/typescript/npm" not in source_path:
  293. ymake.report_configure_error(
  294. "\n"
  295. "Project is configured to use npm as a package manager. \n"
  296. "Only pnpm is supported at the moment.\n"
  297. "Please follow the instruction to migrate your project:\n"
  298. "https://docs.yandex-team.ru/frontend-in-arcadia/tutorials/migrate#migrate-to-pnpm"
  299. )
  300. unit.on_peerdir_ts_resource(pm_type)
  301. unit.set(["PM_TYPE", pm_type])
  302. unit.set(["PM_SCRIPT", f"${pm_type.upper()}_SCRIPT"])
  303. @_with_report_configure_error
  304. def on_set_append_with_directive(unit: NotsUnitType, var_name: str, directive: str, *values: str) -> None:
  305. wrapped = [f'${{{directive}:"{v}"}}' for v in values]
  306. __set_append(unit, var_name, " ".join(wrapped))
  307. def _check_nodejs_version(unit: NotsUnitType, major: int) -> None:
  308. if major < 14:
  309. raise Exception(
  310. "Node.js {} is unsupported. Update Node.js please. See https://nda.ya.ru/t/joB9Mivm6h4znu".format(major)
  311. )
  312. if major < 18:
  313. unit.message(
  314. [
  315. "WARN",
  316. "Node.js {} is deprecated. Update Node.js please. See https://nda.ya.ru/t/joB9Mivm6h4znu".format(major),
  317. ]
  318. )
  319. @_with_report_configure_error
  320. def on_peerdir_ts_resource(unit: NotsUnitType, *resources: str) -> None:
  321. from lib.nots.package_manager import BasePackageManager
  322. pj = BasePackageManager.load_package_json_from_dir(unit.resolve(_get_source_path(unit)))
  323. erm_json = _create_erm_json(unit)
  324. dirs = []
  325. nodejs_version = _select_matching_version(erm_json, "nodejs", pj.get_nodejs_version())
  326. _check_nodejs_version(unit, nodejs_version.major)
  327. for tool in resources:
  328. dir_name = erm_json.canonize_name(tool)
  329. if erm_json.use_resource_directly(tool):
  330. # raises the configuration error when the version is unsupported
  331. _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool), dep_is_required=True)
  332. elif tool == "nodejs":
  333. dirs.append(os.path.join("build", "platform", dir_name, str(nodejs_version)))
  334. _set_resource_vars(unit, erm_json, tool, nodejs_version)
  335. elif erm_json.is_resource_multiplatform(tool):
  336. v = _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool))
  337. sb_resources = [
  338. sbr for sbr in erm_json.get_sb_resources(tool, v) if sbr.get("nodejs") == nodejs_version.major
  339. ]
  340. nodejs_dir = "NODEJS_{}".format(nodejs_version.major)
  341. if len(sb_resources) > 0:
  342. dirs.append(os.path.join("build", "external_resources", dir_name, str(v), nodejs_dir))
  343. _set_resource_vars(unit, erm_json, tool, v, nodejs_version.major)
  344. else:
  345. unit.message(["WARN", "Missing {}@{} for {}".format(tool, str(v), nodejs_dir)])
  346. else:
  347. v = _select_matching_version(erm_json, tool, pj.get_dep_specifier(tool))
  348. dirs.append(os.path.join("build", "external_resources", dir_name, str(v)))
  349. _set_resource_vars(unit, erm_json, tool, v, nodejs_version.major)
  350. if dirs:
  351. unit.onpeerdir(dirs)
  352. @_with_report_configure_error
  353. def on_ts_configure(unit: NotsUnitType) -> None:
  354. from lib.nots.package_manager.base import PackageJson
  355. from lib.nots.package_manager.base.utils import build_pj_path
  356. from lib.nots.typescript import TsConfig
  357. tsconfig_paths = unit.get("TS_CONFIG_PATH").split()
  358. # for use in CMD as inputs
  359. __set_append(
  360. unit, "TS_CONFIG_FILES", _build_cmd_input_paths(tsconfig_paths, hide=True, disable_include_processor=True)
  361. )
  362. mod_dir = unit.get("MODDIR")
  363. cur_dir = unit.get("TS_TEST_FOR_PATH") if unit.get("TS_TEST_FOR") else mod_dir
  364. pj_path = build_pj_path(unit.resolve(unit.resolve_arc_path(cur_dir)))
  365. dep_paths = PackageJson.load(pj_path).get_dep_paths_by_names()
  366. # reversed for using the first tsconfig as the config for include processor (legacy)
  367. for tsconfig_path in reversed(tsconfig_paths):
  368. abs_tsconfig_path = unit.resolve(unit.resolve_arc_path(tsconfig_path))
  369. if not abs_tsconfig_path:
  370. raise Exception("tsconfig not found: {}".format(tsconfig_path))
  371. tsconfig = TsConfig.load(abs_tsconfig_path)
  372. config_files = tsconfig.inline_extend(dep_paths)
  373. config_files = [rootrel_arc_src(path, unit) for path in config_files]
  374. use_tsconfig_outdir = unit.get("TS_CONFIG_USE_OUTDIR") == "yes"
  375. tsconfig.validate(use_tsconfig_outdir)
  376. # add tsconfig files from which root tsconfig files were extended
  377. __set_append(
  378. unit, "TS_CONFIG_FILES", _build_cmd_input_paths(config_files, hide=True, disable_include_processor=True)
  379. )
  380. # region include processor
  381. unit.set(["TS_CONFIG_ROOT_DIR", tsconfig.compiler_option("rootDir")]) # also for hermione
  382. if use_tsconfig_outdir:
  383. unit.set(["TS_CONFIG_OUT_DIR", tsconfig.compiler_option("outDir")]) # also for hermione
  384. unit.set(["TS_CONFIG_SOURCE_MAP", to_yesno(tsconfig.compiler_option("sourceMap"))])
  385. unit.set(["TS_CONFIG_DECLARATION", to_yesno(tsconfig.compiler_option("declaration"))])
  386. unit.set(["TS_CONFIG_DECLARATION_MAP", to_yesno(tsconfig.compiler_option("declarationMap"))])
  387. unit.set(["TS_CONFIG_PRESERVE_JSX", to_yesno(tsconfig.compiler_option("jsx") == "preserve")])
  388. # endregion
  389. _filter_inputs_by_rules_from_tsconfig(unit, tsconfig)
  390. # Code navigation
  391. if unit.get("TS_YNDEXING") == "yes":
  392. unit.on_do_ts_yndexing()
  393. # Style tests
  394. _setup_eslint(unit)
  395. _setup_tsc_typecheck(unit)
  396. _setup_stylelint(unit)
  397. @_with_report_configure_error
  398. def on_setup_build_env(unit: NotsUnitType) -> None:
  399. build_env_var = unit.get("TS_BUILD_ENV")
  400. if not build_env_var:
  401. return
  402. options = []
  403. for name in build_env_var.split(","):
  404. options.append("--env")
  405. value = unit.get(f"TS_ENV_{name}")
  406. if value is None:
  407. ymake.report_configure_error(f"Env var '{name}' is provided in a list, but var value is not provided")
  408. continue
  409. double_quote_escaped_value = value.replace('"', '\\"')
  410. options.append(f'"{name}={double_quote_escaped_value}"')
  411. unit.set(["NOTS_TOOL_BUILD_ENV", " ".join(options)])
  412. def __set_append(unit: NotsUnitType, var_name: str, value: UnitType.PluginArgs, delimiter: str = " ") -> None:
  413. """
  414. SET_APPEND() python naive implementation - append value/values to the list of values
  415. """
  416. old_str_value = unit.get(var_name)
  417. old_values = [old_str_value] if old_str_value else []
  418. new_values = list(value) if isinstance(value, list) or isinstance(value, tuple) else [value]
  419. unit.set([var_name, delimiter.join(old_values + new_values)])
  420. def __strip_prefix(prefix: str, line: str) -> str:
  421. if line.startswith(prefix):
  422. prefix_len = len(prefix)
  423. return line[prefix_len:]
  424. return line
  425. def _filter_inputs_by_rules_from_tsconfig(unit: NotsUnitType, tsconfig: 'TsConfig') -> None:
  426. """
  427. Reduce file list from the TS_GLOB_FILES variable following tsconfig.json rules
  428. """
  429. mod_dir = unit.get("MODDIR")
  430. target_path = os.path.join("${ARCADIA_ROOT}", mod_dir, "") # To have "/" in the end
  431. all_files = [__strip_prefix(target_path, f) for f in unit.get("TS_GLOB_FILES").split(" ")]
  432. filtered_files = tsconfig.filter_files(all_files)
  433. __set_append(unit, "TS_INPUT_FILES", [os.path.join(target_path, f) for f in filtered_files])
  434. def _is_tests_enabled(unit: NotsUnitType) -> bool:
  435. return unit.get("TIDY") != "yes"
  436. def _setup_eslint(unit: NotsUnitType) -> None:
  437. if not _is_tests_enabled(unit):
  438. return
  439. if unit.get("_NO_LINT_VALUE") == "none":
  440. return
  441. test_files = df.TestFiles.ts_lint_srcs(unit, (), {})[df.TestFiles.KEY]
  442. if not test_files:
  443. return
  444. unit.on_peerdir_ts_resource("eslint")
  445. user_recipes = unit.get("TEST_RECIPES_VALUE")
  446. unit.on_setup_install_node_modules_recipe()
  447. test_type = TsTestType.ESLINT
  448. from lib.nots.package_manager import constants
  449. peers = _create_pm(unit).get_peers_from_package_json()
  450. deps = df.CustomDependencies.nots_with_recipies(unit, (peers,), {})[df.CustomDependencies.KEY].split()
  451. if deps:
  452. joined_deps = "\n".join(deps)
  453. logger.info(f"{test_type} deps: \n{joined_deps}")
  454. unit.ondepends(deps)
  455. flat_args = (test_type, "MODDIR")
  456. dart_record = create_dart_record(
  457. TS_TEST_FIELDS_BASE + TS_TEST_SPECIFIC_FIELDS[test_type],
  458. unit,
  459. flat_args,
  460. {},
  461. )
  462. dart_record[df.TestFiles.KEY] = test_files
  463. dart_record[df.NodeModulesBundleFilename.KEY] = constants.NODE_MODULES_WORKSPACE_BUNDLE_FILENAME
  464. extra_deps = df.CustomDependencies.test_depends_only(unit, (), {})[df.CustomDependencies.KEY].split()
  465. dart_record[df.CustomDependencies.KEY] = " ".join(sort_uniq(deps + extra_deps))
  466. if unit.get("TS_LOCAL_CLI") != "yes":
  467. # disable chunks for `ya tool nots`
  468. dart_record[df.LintFileProcessingTime.KEY] = str(ESLINT_FILE_PROCESSING_TIME_DEFAULT)
  469. data = ytest.dump_test(unit, dart_record)
  470. if data:
  471. unit.set_property(["DART_DATA", data])
  472. unit.set(["TEST_RECIPES_VALUE", user_recipes])
  473. @_with_report_configure_error
  474. def _setup_tsc_typecheck(unit: NotsUnitType) -> None:
  475. if not _is_tests_enabled(unit):
  476. return
  477. if unit.get("_TS_TYPECHECK_VALUE") == "none":
  478. return
  479. test_files = df.TestFiles.ts_input_files(unit, (), {})[df.TestFiles.KEY]
  480. if not test_files:
  481. return
  482. tsconfig_paths = unit.get("TS_CONFIG_PATH").split()
  483. tsconfig_path = tsconfig_paths[0]
  484. if len(tsconfig_paths) > 1:
  485. tsconfig_path = unit.get("_TS_TYPECHECK_TSCONFIG")
  486. if not tsconfig_path:
  487. macros = " or ".join([f"TS_TYPECHECK({p})" for p in tsconfig_paths])
  488. raise Exception(f"Module uses several tsconfig files, specify which one to use for typecheck: {macros}")
  489. abs_tsconfig_path = unit.resolve(unit.resolve_arc_path(tsconfig_path))
  490. if not abs_tsconfig_path:
  491. raise Exception(f"tsconfig for typecheck not found: {tsconfig_path}")
  492. unit.on_peerdir_ts_resource("typescript")
  493. user_recipes = unit.get("TEST_RECIPES_VALUE")
  494. unit.on_setup_install_node_modules_recipe()
  495. unit.on_setup_extract_output_tars_recipe([unit.get("MODDIR")])
  496. test_type = TsTestType.TSC_TYPECHECK
  497. from lib.nots.package_manager import constants
  498. peers = _create_pm(unit).get_peers_from_package_json()
  499. deps = df.CustomDependencies.nots_with_recipies(unit, (peers,), {})[df.CustomDependencies.KEY].split()
  500. if deps:
  501. joined_deps = "\n".join(deps)
  502. logger.info(f"{test_type} deps: \n{joined_deps}")
  503. unit.ondepends(deps)
  504. flat_args = (test_type,)
  505. dart_record = create_dart_record(
  506. TS_TEST_FIELDS_BASE + TS_TEST_SPECIFIC_FIELDS[test_type],
  507. unit,
  508. flat_args,
  509. {},
  510. )
  511. dart_record[df.TestFiles.KEY] = test_files
  512. dart_record[df.NodeModulesBundleFilename.KEY] = constants.NODE_MODULES_WORKSPACE_BUNDLE_FILENAME
  513. extra_deps = df.CustomDependencies.test_depends_only(unit, (), {})[df.CustomDependencies.KEY].split()
  514. dart_record[df.CustomDependencies.KEY] = " ".join(sort_uniq(deps + extra_deps))
  515. dart_record[df.TsConfigPath.KEY] = tsconfig_path
  516. data = ytest.dump_test(unit, dart_record)
  517. if data:
  518. unit.set_property(["DART_DATA", data])
  519. unit.set(["TEST_RECIPES_VALUE", user_recipes])
  520. @_with_report_configure_error
  521. def _setup_stylelint(unit: NotsUnitType) -> None:
  522. if not _is_tests_enabled(unit):
  523. return
  524. if unit.get("_TS_STYLELINT_VALUE") == "no":
  525. return
  526. test_files = df.TestFiles.stylesheets(unit, (), {})[df.TestFiles.KEY]
  527. if not test_files:
  528. return
  529. unit.on_peerdir_ts_resource("stylelint")
  530. from lib.nots.package_manager import constants
  531. recipes_value = unit.get("TEST_RECIPES_VALUE")
  532. unit.on_setup_install_node_modules_recipe()
  533. unit.on_setup_extract_output_tars_recipe([unit.get("MODDIR")])
  534. test_type = TsTestType.TS_STYLELINT
  535. peers = _create_pm(unit).get_peers_from_package_json()
  536. deps = df.CustomDependencies.nots_with_recipies(unit, (peers,), {})[df.CustomDependencies.KEY].split()
  537. if deps:
  538. joined_deps = "\n".join(deps)
  539. logger.info(f"{test_type} deps: \n{joined_deps}")
  540. unit.ondepends(deps)
  541. flat_args = (test_type,)
  542. spec_args = dict(nm_bundle=constants.NODE_MODULES_WORKSPACE_BUNDLE_FILENAME)
  543. dart_record = create_dart_record(
  544. TS_TEST_FIELDS_BASE + TS_TEST_SPECIFIC_FIELDS[test_type], unit, flat_args, spec_args
  545. )
  546. extra_deps = df.CustomDependencies.test_depends_only(unit, (), {})[df.CustomDependencies.KEY].split()
  547. dart_record[df.CustomDependencies.KEY] = " ".join(sort_uniq(deps + extra_deps))
  548. data = ytest.dump_test(unit, dart_record)
  549. if data:
  550. unit.set_property(["DART_DATA", data])
  551. unit.set(["TEST_RECIPES_VALUE", recipes_value])
  552. def _set_resource_vars(
  553. unit: NotsUnitType, erm_json: 'ErmJsonLite', tool: str, version: 'Version', nodejs_major: int = None
  554. ) -> None:
  555. resource_name = erm_json.canonize_name(tool).upper()
  556. # example: NODEJS_12_18_4 | HERMIONE_7_0_4_NODEJS_18
  557. version_str = str(version).replace(".", "_")
  558. yamake_resource_name = "{}_{}".format(resource_name, version_str)
  559. if erm_json.is_resource_multiplatform(tool):
  560. yamake_resource_name += "_NODEJS_{}".format(nodejs_major)
  561. yamake_resource_var = "{}_RESOURCE_GLOBAL".format(yamake_resource_name)
  562. unit.set(["{}_ROOT".format(resource_name), "${}".format(yamake_resource_var)])
  563. unit.set(["{}-ROOT-VAR-NAME".format(resource_name), yamake_resource_var])
  564. def _select_matching_version(
  565. erm_json: 'ErmJsonLite', resource_name: str, range_str: str, dep_is_required=False
  566. ) -> 'Version':
  567. if dep_is_required and range_str is None:
  568. raise Exception(
  569. "Please install the '{tool}' package to the project. Run the command:\n"
  570. " ya tool nots add -D {tool}".format(tool=resource_name)
  571. )
  572. try:
  573. version = erm_json.select_version_of(resource_name, range_str)
  574. if version:
  575. return version
  576. raise ValueError("There is no allowed version to satisfy this range: '{}'".format(range_str))
  577. except Exception as error:
  578. toolchain_versions = erm_json.get_versions_of(erm_json.get_resource(resource_name))
  579. raise Exception(
  580. "Requested {} version range '{}' could not be satisfied. \n"
  581. "Please use a range that would include one of the following: {}. \n"
  582. "For further details please visit the link: {} \nOriginal error: {} \n".format(
  583. resource_name,
  584. range_str,
  585. ", ".join(map(str, toolchain_versions)),
  586. "https://docs.yandex-team.ru/frontend-in-arcadia/_generated/toolchain",
  587. str(error),
  588. )
  589. )
  590. @_with_report_configure_error
  591. def on_prepare_deps_configure(unit: NotsUnitType) -> None:
  592. pm = _create_pm(unit)
  593. pj = pm.load_package_json_from_dir(pm.sources_path)
  594. has_deps = pj.has_dependencies()
  595. ins, outs, resources = pm.calc_prepare_deps_inouts_and_resources(unit.get("_TARBALLS_STORE"), has_deps)
  596. if has_deps:
  597. unit.onpeerdir(pm.get_local_peers_from_package_json())
  598. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives(["hide", "input"], sorted(ins)))
  599. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives(["hide", "output"], sorted(outs)))
  600. unit.set(["_PREPARE_DEPS_RESOURCES", " ".join([f'${{resource:"{uri}"}}' for uri in sorted(resources)])])
  601. unit.set(["_PREPARE_DEPS_USE_RESOURCES_FLAG", "--resource-root $(RESOURCE_ROOT)"])
  602. else:
  603. __set_append(unit, "_PREPARE_DEPS_INOUTS", _build_directives(["output"], sorted(outs)))
  604. unit.set(["_PREPARE_DEPS_CMD", "$_PREPARE_NO_DEPS_CMD"])
  605. @_with_report_configure_error
  606. def on_node_modules_configure(unit: NotsUnitType) -> None:
  607. pm = _create_pm(unit)
  608. pj = pm.load_package_json_from_dir(pm.sources_path)
  609. has_deps = pj.has_dependencies()
  610. if has_deps:
  611. unit.onpeerdir(pm.get_local_peers_from_package_json())
  612. local_cli = unit.get("TS_LOCAL_CLI") == "yes"
  613. ins, outs = pm.calc_node_modules_inouts(local_cli, has_deps)
  614. __set_append(unit, "_NODE_MODULES_INOUTS", _build_directives(["hide", "input"], sorted(ins)))
  615. if not unit.get("TS_TEST_FOR"):
  616. __set_append(unit, "_NODE_MODULES_INOUTS", _build_directives(["hide", "output"], sorted(outs)))
  617. if pj.get_use_prebuilder():
  618. unit.on_peerdir_ts_resource("@yatool/prebuilder")
  619. unit.set(
  620. [
  621. "_YATOOL_PREBUILDER_ARG",
  622. "--yatool-prebuilder-path $YATOOL_PREBUILDER_ROOT/node_modules/@yatool/prebuilder",
  623. ]
  624. )
  625. # YATOOL_PREBUILDER_0_7_0_RESOURCE_GLOBAL
  626. prebuilder_major = unit.get("YATOOL_PREBUILDER-ROOT-VAR-NAME").split("_")[2]
  627. logger.info(f"Detected prebuilder {COLORS.green}{prebuilder_major}.x.x{COLORS.reset}")
  628. if prebuilder_major == "0":
  629. # TODO: FBP-1408
  630. lf = pm.load_lockfile_from_dir(pm.sources_path)
  631. is_valid, invalid_keys = lf.validate_has_addons_flags()
  632. if not is_valid:
  633. ymake.report_configure_error(
  634. "Project is configured to use @yatool/prebuilder. \n"
  635. + "Some packages in the pnpm-lock.yaml are misconfigured.\n"
  636. + f"Run {COLORS.green}`ya tool nots update-lockfile`{COLORS.reset} to fix lockfile.\n"
  637. + "All packages with `requiresBuild:true` have to be marked with `hasAddons:true/false`.\n"
  638. + "Misconfigured keys: \n"
  639. + " - "
  640. + "\n - ".join(invalid_keys)
  641. )
  642. else:
  643. lf = pm.load_lockfile_from_dir(pm.sources_path)
  644. requires_build_packages = lf.get_requires_build_packages()
  645. is_valid, validation_messages = pj.validate_prebuilds(requires_build_packages)
  646. if not is_valid:
  647. ymake.report_configure_error(
  648. "Project is configured to use @yatool/prebuilder. \n"
  649. + "Some packages are misconfigured.\n"
  650. + f"Run {COLORS.green}`ya tool nots update-lockfile`{COLORS.reset} to fix pnpm-lock.yaml and package.json.\n"
  651. + "Validation details: \n"
  652. + "\n".join(validation_messages)
  653. )
  654. @_with_report_configure_error
  655. def on_ts_test_for_configure(
  656. unit: NotsUnitType, test_runner: TsTestType, default_config: str, node_modules_filename: str
  657. ) -> None:
  658. if not _is_tests_enabled(unit):
  659. return
  660. if unit.enabled('TS_COVERAGE'):
  661. unit.on_peerdir_ts_resource("nyc")
  662. for_mod_path = df.TsTestForPath.value(unit, (), {})[df.TsTestForPath.KEY]
  663. unit.onpeerdir([for_mod_path])
  664. unit.on_setup_extract_node_modules_recipe([for_mod_path])
  665. unit.on_setup_extract_output_tars_recipe([for_mod_path])
  666. build_root = "$B" if test_runner in [TsTestType.HERMIONE, TsTestType.PLAYWRIGHT_LARGE] else "$(BUILD_ROOT)"
  667. unit.set(["TS_TEST_NM", os.path.join(build_root, for_mod_path, node_modules_filename)])
  668. config_path = unit.get("TS_TEST_CONFIG_PATH")
  669. if not config_path:
  670. config_path = os.path.join(for_mod_path, default_config)
  671. unit.set(["TS_TEST_CONFIG_PATH", config_path])
  672. test_files = df.TestFiles.ts_test_srcs(unit, (), {})[df.TestFiles.KEY]
  673. if not test_files:
  674. ymake.report_configure_error("No tests found")
  675. return
  676. from lib.nots.package_manager import constants
  677. peers = _create_pm(unit).get_peers_from_package_json()
  678. deps = df.CustomDependencies.nots_with_recipies(unit, (peers,), {})[df.CustomDependencies.KEY].split()
  679. if deps:
  680. joined_deps = "\n".join(deps)
  681. logger.info(f"{test_runner} deps: \n{joined_deps}")
  682. unit.ondepends(deps)
  683. flat_args = (test_runner, "TS_TEST_FOR_PATH")
  684. spec_args = {'erm_json': _create_erm_json(unit)}
  685. dart_record = create_dart_record(
  686. TS_TEST_FIELDS_BASE + TS_TEST_SPECIFIC_FIELDS[test_runner],
  687. unit,
  688. flat_args,
  689. spec_args,
  690. )
  691. dart_record[df.TestFiles.KEY] = test_files
  692. dart_record[df.NodeModulesBundleFilename.KEY] = constants.NODE_MODULES_WORKSPACE_BUNDLE_FILENAME
  693. extra_deps = df.CustomDependencies.test_depends_only(unit, (), {})[df.CustomDependencies.KEY].split()
  694. dart_record[df.CustomDependencies.KEY] = " ".join(sort_uniq(deps + extra_deps))
  695. if test_runner in [TsTestType.HERMIONE, TsTestType.PLAYWRIGHT_LARGE]:
  696. dart_record[df.Size.KEY] = "LARGE"
  697. data = ytest.dump_test(unit, dart_record)
  698. if data:
  699. unit.set_property(["DART_DATA", data])
  700. # noinspection PyUnusedLocal
  701. @_with_report_configure_error
  702. def on_validate_ts_test_for_args(unit: NotsUnitType, for_mod: str, root: str) -> None:
  703. # FBP-1085
  704. is_arc_root = root == "${ARCADIA_ROOT}"
  705. is_rel_for_mod = for_mod.startswith(".")
  706. if is_arc_root and is_rel_for_mod:
  707. ymake.report_configure_error(
  708. "You are using a relative path for a module. "
  709. + "You have to add RELATIVE key, like (RELATIVE {})".format(for_mod)
  710. )
  711. @_with_report_configure_error
  712. def on_set_ts_test_for_vars(unit: NotsUnitType, for_mod: str) -> None:
  713. unit.set(["TS_TEST_FOR", "yes"])
  714. unit.set(["TS_TEST_FOR_DIR", unit.resolve_arc_path(for_mod)])
  715. unit.set(["TS_TEST_FOR_PATH", rootrel_arc_src(for_mod, unit)])
  716. def __on_ts_files(unit: NotsUnitType, files_in: list[str], files_out: list[str]) -> None:
  717. for f in files_in:
  718. if f.startswith(".."):
  719. ymake.report_configure_error(
  720. "TS_FILES* macroses are only allowed to use files inside the project directory.\n"
  721. f"Got path '{f}'.\n"
  722. "Docs: https://docs.yandex-team.ru/frontend-in-arcadia/references/TS_PACKAGE#ts-files."
  723. )
  724. new_items = _build_cmd_input_paths(paths=files_in, hide=True, disable_include_processor=True)
  725. new_items += _build_cmd_output_paths(paths=files_out, hide=True)
  726. __set_append(unit, "_TS_FILES_INOUTS", new_items)
  727. logger.print_vars("_TS_FILES_INOUTS")
  728. @_with_report_configure_error
  729. def on_ts_files(unit: NotsUnitType, *files: str) -> None:
  730. __on_ts_files(unit, files, files)
  731. @_with_report_configure_error
  732. def on_ts_large_files(unit: NotsUnitType, destination: str, *files: list[str]) -> None:
  733. if destination == REQUIRED_MISSING:
  734. ymake.report_configure_error(
  735. "Macro TS_LARGE_FILES() requires to use DESTINATION parameter.\n"
  736. " TS_LARGE_FILES(\n"
  737. " DESTINATION some_dir\n"
  738. " large/file1\n"
  739. " large/file2\n"
  740. " )\n"
  741. "Docs: https://docs.yandex-team.ru/frontend-in-arcadia/references/TS_PACKAGE#ts-large-files."
  742. )
  743. return
  744. in_files = [os.path.join('${BINDIR}', f) for f in files]
  745. out_files = [os.path.join('${BINDIR}', destination, f) for f in files]
  746. # TODO: FBP-1795
  747. # ${BINDIR} prefix for input is important to resolve to result of LARGE_FILES and not to SOURCEDIR
  748. new_items = [f'$COPY_CMD {i} {o}' for (i, o) in zip(in_files, out_files)]
  749. __set_append(unit, "_TS_PROJECT_SETUP_CMD", new_items, " && ")
  750. logger.print_vars("_TS_PROJECT_SETUP_CMD")
  751. __on_ts_files(unit, in_files, out_files)
  752. @_with_report_configure_error
  753. def on_ts_package_check_files(unit: NotsUnitType) -> None:
  754. ts_files = unit.get("_TS_FILES_INOUTS")
  755. if ts_files == "":
  756. ymake.report_configure_error(
  757. "\n"
  758. "In the TS_PACKAGE module, you should define at least one file using the TS_FILES() macro.\n"
  759. "If you use the TS_FILES_GLOB, check the expression. For example, use `src/**/*` instead of `src/*`.\n"
  760. "Docs: https://docs.yandex-team.ru/frontend-in-arcadia/references/TS_PACKAGE#ts-files."
  761. )
  762. @_with_report_configure_error
  763. def on_depends_on_mod(unit: NotsUnitType) -> None:
  764. if unit.get("_TS_TEST_DEPENDS_ON_BUILD"):
  765. for_mod_path = unit.get("TS_TEST_FOR_PATH")
  766. unit.ondepends([for_mod_path])
  767. @_with_report_configure_error
  768. def on_run_javascript_after_build_add_js_script_as_input(unit: NotsUnitType, js_script: str) -> None:
  769. js_script = os.path.normpath(js_script)
  770. if js_script.startswith("node_modules/"):
  771. return
  772. __set_append(unit, "_RUN_JAVASCRIPT_AFTER_BUILD_INPUTS", js_script)
  773. # Zero-diff commit