nots.py 31 KB

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