1/** 2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16#include "arktsconfig.h" 17#include "libpandabase/utils/json_builder.h" 18#include "libpandabase/utils/json_parser.h" 19#include "libpandabase/os/filesystem.h" 20#include "util/language.h" 21#include "generated/signatures.h" 22 23#include <fstream> 24#include <regex> 25#include <sstream> 26#include <system_error> 27 28#ifndef ARKTSCONFIG_USE_FILESYSTEM 29#include <dirent.h> 30#include <sys/types.h> 31#include <unistd.h> 32#else 33#if __has_include(<filesystem>) 34#include <filesystem> 35namespace fs = std::filesystem; 36#elif __has_include(<experimental/filesystem>) 37#include <experimental/filesystem> 38namespace fs = std::experimental::filesystem; 39#endif 40#endif 41 42namespace ark::es2panda { 43 44template <class... Ts> 45static bool Check(bool cond, const Ts &...msgs) 46{ 47 if (!cond) { 48 ((std::cerr << "ArkTsConfig error: ") << ... << msgs); 49 return false; 50 } 51 52 return true; 53} 54 55static bool IsAbsolute(const std::string &path) 56{ 57#ifndef ARKTSCONFIG_USE_FILESYSTEM 58 return !path.empty() && path[0] == '/'; 59#else 60 return fs::path(path).is_absolute(); 61#endif // ARKTSCONFIG_USE_FILESYSTEM 62} 63 64std::string JoinPaths(const std::string &a, const std::string &b) 65{ 66#ifndef ARKTSCONFIG_USE_FILESYSTEM 67 return a + '/' + b; 68#else 69 return (fs::path(a) / b).string(); 70#endif // ARKTSCONFIG_USE_FILESYSTEM 71} 72 73std::string ParentPath(const std::string &path) 74{ 75#ifndef ARKTSCONFIG_USE_FILESYSTEM 76 auto pos = path.find('/'); 77 return pos == std::string::npos ? path : path.substr(0, pos); 78#else 79 return fs::path(path).parent_path().string(); 80#endif // ARKTSCONFIG_USE_FILESYSTEM 81} 82 83static std::string MakeAbsolute(const std::string &path, const std::string &base) 84{ 85 return IsAbsolute(path) ? path : JoinPaths(base, path); 86} 87 88#ifdef ARKTSCONFIG_USE_FILESYSTEM 89 90ArkTsConfig::Pattern::Pattern(std::string value, std::string base) : value_(std::move(value)), base_(std::move(base)) 91{ 92 ASSERT(fs::path(base_).is_absolute()); 93} 94 95bool ArkTsConfig::Pattern::IsPattern() const 96{ 97 return (value_.find('*') != std::string::npos) || (value_.find('?') != std::string::npos); 98} 99 100std::string ArkTsConfig::Pattern::GetSearchRoot() const 101{ 102 fs::path relative; 103 if (!IsPattern()) { 104 relative = value_; 105 } else { 106 auto foundStar = value_.find_first_of('*'); 107 auto foundQuestion = value_.find_first_of('?'); 108 relative = value_.substr(0, std::min(foundStar, foundQuestion)); 109 relative = relative.parent_path(); 110 } 111 return MakeAbsolute(relative.string(), base_); 112} 113 114bool ArkTsConfig::Pattern::Match(const std::string &path) const 115{ 116 ASSERT(fs::path(path).is_absolute()); 117 fs::path value = fs::path(value_); 118 std::string pattern = value.is_absolute() ? value.string() : (base_ / value).string(); 119 120 // Replace arktsconfig special symbols with regular expressions 121 if (IsPattern()) { 122 // '**' matches any directory nested to any level 123 pattern = std::regex_replace(pattern, std::regex("\\*\\*/"), ".*"); 124 // '*' matches zero or more characters (excluding directory separators) 125 pattern = std::regex_replace(pattern, std::regex("([^\\.])\\*"), "$1[^/]*"); 126 // '?' matches any one character (excluding directory separators) 127 pattern = std::regex_replace(pattern, std::regex("\\?"), "[^/]"); 128 } 129 if (!value.has_extension()) { 130 // default extensions to match 131 pattern += R"(.*(\.ts|\.d\.ts|\.sts)$)"; 132 } 133 std::smatch m; 134 auto res = std::regex_match(path, m, std::regex(pattern)); 135 return res; 136} 137 138static std::string ResolveConfigLocation(const std::string &relPath, const std::string &base) 139{ 140 auto resolvedPath = MakeAbsolute(relPath, base); 141 auto newBase = base; 142 while (!fs::exists(resolvedPath)) { 143 resolvedPath = MakeAbsolute(relPath, JoinPaths(newBase, "node_modules")); 144 if (newBase == ParentPath(newBase)) { 145 return ""; 146 } 147 newBase = ParentPath(newBase); 148 } 149 return resolvedPath; 150} 151 152bool ArkTsConfig::ParseExtends(const std::string &extends, const std::string &configDir) 153{ 154 auto basePath = ResolveConfigLocation(extends, configDir); 155 if (!Check(!basePath.empty(), "Can't resolve config path: ", extends)) { 156 return false; 157 } 158 159 auto base = ArkTsConfig(basePath); 160 if (!Check(base.Parse(), "Failed to parse base config: ", extends)) { 161 return false; 162 } 163 164 Inherit(base); 165 return true; 166} 167#endif // ARKTSCONFIG_USE_FILESYSTEM 168 169static std::string ValidDynamicLanguages() 170{ 171 JsonArrayBuilder builder; 172 for (auto &l : Language::All()) { 173 if (l.IsDynamic()) { 174 builder.Add(l.ToString()); 175 } 176 } 177 return std::move(builder).Build(); 178} 179 180template <class PathsMap> 181static bool ParsePaths(const JsonObject::JsonObjPointer *options, PathsMap &pathsMap, const std::string &baseUrl) 182{ 183 if (options == nullptr) { 184 return true; 185 } 186 187 auto paths = options->get()->GetValue<JsonObject::JsonObjPointer>("paths"); 188 if (paths == nullptr) { 189 return true; 190 } 191 192 for (size_t keyIdx = 0; keyIdx < paths->get()->GetSize(); ++keyIdx) { 193 auto &key = paths->get()->GetKeyByIndex(keyIdx); 194 if (pathsMap.count(key) == 0U) { 195 pathsMap.insert({key, {}}); 196 } 197 198 auto values = paths->get()->GetValue<JsonObject::ArrayT>(key); 199 if (!Check(values, "Invalid value for 'path' with key '", key, "'")) { 200 return false; 201 } 202 203 if (!Check(!values->empty(), "Substitutions for pattern '", key, "' shouldn't be an empty array")) { 204 return false; 205 } 206 207 for (auto &v : *values) { 208 auto p = *v.Get<JsonObject::StringT>(); 209 pathsMap[key].emplace_back(MakeAbsolute(p, baseUrl)); 210 } 211 } 212 213 return true; 214} 215 216template <class PathsMap> 217static bool ParseDynamicPaths(const JsonObject::JsonObjPointer *options, PathsMap &dynamicPathsMap) 218{ 219 static const std::string LANGUAGE = "language"; 220 static const std::string HAS_DECL = "hasDecl"; 221 222 if (options == nullptr) { 223 return true; 224 } 225 226 auto dynamicPaths = options->get()->GetValue<JsonObject::JsonObjPointer>("dynamicPaths"); 227 if (dynamicPaths == nullptr) { 228 return true; 229 } 230 231 for (size_t keyIdx = 0; keyIdx < dynamicPaths->get()->GetSize(); ++keyIdx) { 232 auto &key = dynamicPaths->get()->GetKeyByIndex(keyIdx); 233 auto data = dynamicPaths->get()->GetValue<JsonObject::JsonObjPointer>(key); 234 if (!Check(data != nullptr, "Invalid value for for dynamic path with key '", key, "'")) { 235 return false; 236 } 237 238 auto langValue = data->get()->GetValue<JsonObject::StringT>(LANGUAGE); 239 if (!Check(langValue != nullptr, "Invalid '", LANGUAGE, "' value for dynamic path with key '", key, "'")) { 240 return false; 241 } 242 243 auto lang = Language::FromString(*langValue); 244 if (!Check(lang && lang->IsDynamic(), "Invalid '", LANGUAGE, "' value for dynamic path with key '", key, 245 "'. Should be one of ", ValidDynamicLanguages())) { 246 return false; 247 } 248 249 if (!Check(compiler::Signatures::Dynamic::IsSupported(*lang), "Interoperability with language '", 250 lang->ToString(), "' is not supported")) { 251 return false; 252 } 253 254 auto hasDeclValue = data->get()->GetValue<JsonObject::BoolT>(HAS_DECL); 255 if (!Check(hasDeclValue != nullptr, "Invalid '", HAS_DECL, "' value for dynamic path with key '", key, "'")) { 256 return false; 257 } 258 259 auto normalizedKey = ark::os::NormalizePath(key); 260 auto res = dynamicPathsMap.insert({normalizedKey, ArkTsConfig::DynamicImportData(*lang, *hasDeclValue)}); 261 if (!Check(res.second, "Duplicated dynamic path '", key, "' for key '", key, "'")) { 262 return false; 263 } 264 } 265 266 return true; 267} 268 269template <class Collection, class Function> 270static bool ParseCollection(const JsonObject *config, Collection &out, const std::string &target, 271 Function &&constructor) 272{ 273 auto arr = config->GetValue<JsonObject::ArrayT>(target); 274 if (arr != nullptr) { 275 out = {}; 276 if (!Check(!arr->empty(), "The '", target, "' list in config file is empty")) { 277 return false; 278 } 279 280 for (auto &i : *arr) { 281 out.emplace_back(constructor(*i.Get<JsonObject::StringT>())); 282 } 283 } 284 285 return true; 286} 287 288static std::optional<std::string> ReadConfig(const std::string &path) 289{ 290 std::ifstream inputStream(path); 291 if (!Check(!inputStream.fail(), "Failed to open file: ", path)) { 292 return {}; 293 } 294 295 std::stringstream ss; 296 ss << inputStream.rdbuf(); 297 return ss.str(); 298} 299 300static void ParseRelDir(std::string &dst, const std::string &key, const JsonObject::JsonObjPointer *options, 301 const std::string &configDir) 302{ 303 if (options != nullptr) { 304 auto path = options->get()->GetValue<JsonObject::StringT>(key); 305 dst = ((path != nullptr) ? *path : ""); 306 } 307 308 dst = MakeAbsolute(dst, configDir); 309} 310 311bool ArkTsConfig::Parse() 312{ 313 static const std::string BASE_URL = "baseUrl"; 314 static const std::string COMPILER_OPTIONS = "compilerOptions"; 315 static const std::string EXCLUDE = "exclude"; 316 static const std::string EXTENDS = "extends"; 317 static const std::string FILES = "files"; 318 static const std::string INCLUDE = "include"; 319 static const std::string OUT_DIR = "outDir"; 320 static const std::string ROOT_DIR = "rootDir"; 321 322 ASSERT(!isParsed_); 323 isParsed_ = true; 324 auto arktsConfigDir = ParentPath(ark::os::GetAbsolutePath(configPath_)); 325 326 // Read input 327 auto tsConfigSource = ReadConfig(configPath_); 328 if (!tsConfigSource) { 329 return false; 330 } 331 332 // Parse json 333 auto arktsConfig = std::make_unique<JsonObject>(*tsConfigSource); 334 if (!Check(arktsConfig->IsValid(), "ArkTsConfig is not valid json")) { 335 return false; 336 } 337 338#ifdef ARKTSCONFIG_USE_FILESYSTEM 339 // Parse "extends" 340 auto extends = arktsConfig->GetValue<JsonObject::StringT>(EXTENDS); 341 if (extends != nullptr && !ParseExtends(*extends, arktsConfigDir)) { 342 return false; 343 } 344#endif // ARKTSCONFIG_USE_FILESYSTEM 345 346 auto compilerOptions = arktsConfig->GetValue<JsonObject::JsonObjPointer>(COMPILER_OPTIONS); 347 348 // Parse "baseUrl", "outDir", "rootDir" 349 ParseRelDir(baseUrl_, BASE_URL, compilerOptions, arktsConfigDir); 350 ParseRelDir(outDir_, OUT_DIR, compilerOptions, arktsConfigDir); 351 ParseRelDir(rootDir_, ROOT_DIR, compilerOptions, arktsConfigDir); 352 353 // Parse "paths" 354 if (!ParsePaths(compilerOptions, paths_, baseUrl_) || !ParseDynamicPaths(compilerOptions, dynamicPaths_)) { 355 return false; 356 } 357 358 // Parse "files" 359 auto concatPath = [&arktsConfigDir](const auto &val) { return MakeAbsolute(val, arktsConfigDir); }; 360 if (!ParseCollection(arktsConfig.get(), files_, FILES, concatPath)) { 361 return false; 362 } 363 364#ifdef ARKTSCONFIG_USE_FILESYSTEM 365 // Parse "include" and "exclude" 366 auto consPattern = [&arktsConfigDir](const auto &val) { return Pattern {val, arktsConfigDir}; }; 367 return ParseCollection(arktsConfig.get(), include_, INCLUDE, consPattern) && 368 ParseCollection(arktsConfig.get(), exclude_, EXCLUDE, consPattern); 369#else 370 return true; 371#endif // ARKTSCONFIG_USE_FILESYSTEM 372} 373 374void ArkTsConfig::Inherit(const ArkTsConfig &base) 375{ 376 baseUrl_ = base.baseUrl_; 377 outDir_ = base.outDir_; 378 rootDir_ = base.rootDir_; 379 paths_ = base.paths_; 380 files_ = base.files_; 381#ifdef ARKTSCONFIG_USE_FILESYSTEM 382 include_ = base.include_; 383 exclude_ = base.exclude_; 384#endif // ARKTSCONFIG_USE_FILESYSTEM 385} 386 387// Remove '/' and '*' from the end of path 388static std::string TrimPath(const std::string &path) 389{ 390 std::string trimmedPath = path; 391 while (!trimmedPath.empty() && (trimmedPath.back() == '*' || trimmedPath.back() == '/')) { 392 trimmedPath.pop_back(); 393 } 394 return trimmedPath; 395} 396 397std::optional<std::string> ArkTsConfig::ResolvePath(const std::string &path) const 398{ 399 for (const auto &[alias, paths] : paths_) { 400 auto trimmedAlias = TrimPath(alias); 401 size_t pos = path.rfind(trimmedAlias, 0); 402 if (pos == 0) { 403 std::string resolved = path; 404 // NOTE(ivagin): arktsconfig contains array of paths for each prefix, for now just get first one 405 std::string newPrefix = TrimPath(paths[0]); 406 resolved.replace(pos, trimmedAlias.length(), newPrefix); 407 return resolved; 408 } 409 } 410 return std::nullopt; 411} 412 413#ifdef ARKTSCONFIG_USE_FILESYSTEM 414static bool MatchExcludes(const fs::path &path, const std::vector<ArkTsConfig::Pattern> &excludes) 415{ 416 for (auto &e : excludes) { 417 if (e.Match(path.string())) { 418 return true; 419 } 420 } 421 return false; 422} 423 424static std::vector<fs::path> GetSourceList(const std::shared_ptr<ArkTsConfig> &arktsConfig) 425{ 426 auto includes = arktsConfig->Include(); 427 auto excludes = arktsConfig->Exclude(); 428 auto files = arktsConfig->Files(); 429 430 // If "files" and "includes" are empty - include everything from tsconfig root 431 auto configDir = fs::absolute(fs::path(arktsConfig->ConfigPath())).parent_path(); 432 if (files.empty() && includes.empty()) { 433 includes = {ArkTsConfig::Pattern("**/*", configDir.string())}; 434 } 435 // If outDir in not default add it into exclude 436 if (!fs::equivalent(arktsConfig->OutDir(), configDir)) { 437 excludes.emplace_back("**/*", arktsConfig->OutDir()); 438 } 439 440 // Collect "files" 441 std::vector<fs::path> sourceList; 442 for (auto &f : files) { 443 if (!Check(fs::exists(f) && fs::path(f).has_filename(), "No such file: ", f)) { 444 return {}; 445 } 446 447 sourceList.emplace_back(f); 448 } 449 450 // Collect "include" 451 // TSC traverses folders for sources starting from 'include' rather than from 'rootDir', so we do the same 452 for (auto &include : includes) { 453 auto traverseRoot = fs::path(include.GetSearchRoot()); 454 if (!fs::exists(traverseRoot)) { 455 continue; 456 } 457 if (!fs::is_directory(traverseRoot)) { 458 if (include.Match(traverseRoot.string()) && !MatchExcludes(traverseRoot, excludes)) { 459 sourceList.emplace_back(traverseRoot); 460 } 461 continue; 462 } 463 for (const auto &dirEntry : fs::recursive_directory_iterator(traverseRoot)) { 464 if (include.Match(dirEntry.path().string()) && !MatchExcludes(dirEntry, excludes)) { 465 sourceList.emplace_back(dirEntry); 466 } 467 } 468 } 469 return sourceList; 470} 471 472// Analogue of 'std::filesystem::relative()' 473// Example: Relative("/a/b/c", "/a/b") returns "c" 474static fs::path Relative(const fs::path &src, const fs::path &base) 475{ 476 fs::path tmpPath = src; 477 fs::path relPath; 478 while (!fs::equivalent(tmpPath, base)) { 479 relPath = relPath.empty() ? tmpPath.filename() : tmpPath.filename() / relPath; 480 if (tmpPath == tmpPath.parent_path()) { 481 return fs::path(); 482 } 483 tmpPath = tmpPath.parent_path(); 484 } 485 return relPath; 486} 487 488// Compute path to destination file and create subfolders 489static fs::path ComputeDestination(const fs::path &src, const fs::path &rootDir, const fs::path &outDir) 490{ 491 auto rel = Relative(src, rootDir); 492 if (!Check(!rel.empty(), rootDir, " is not root directory for ", src)) { 493 return {}; 494 } 495 496 auto dst = outDir / rel; 497 fs::create_directories(dst.parent_path()); 498 return dst.replace_extension("abc"); 499} 500 501std::vector<std::pair<std::string, std::string>> FindProjectSources(const std::shared_ptr<ArkTsConfig> &arktsConfig) 502{ 503 auto sourceFiles = GetSourceList(arktsConfig); 504 std::vector<std::pair<std::string, std::string>> compilationList; 505 for (auto &src : sourceFiles) { 506 auto dst = ComputeDestination(src, arktsConfig->RootDir(), arktsConfig->OutDir()); 507 if (!Check(!dst.empty(), "Invalid destination file")) { 508 return {}; 509 } 510 511 compilationList.emplace_back(src.string(), dst.string()); 512 } 513 514 return compilationList; 515} 516#else 517std::vector<std::pair<std::string, std::string>> FindProjectSources( 518 [[maybe_unused]] const std::shared_ptr<ArkTsConfig> &arkts_config) 519{ 520 ASSERT(false); 521 return {}; 522} 523#endif // ARKTSCONFIG_USE_FILESYSTEM 524 525} // namespace ark::es2panda 526