const _global = @import("../global.zig"); const string = _global.string; const Output = _global.Output; const Global = _global.Global; const Environment = _global.Environment; const strings = _global.strings; const MutableString = _global.MutableString; const stringZ = _global.stringZ; const default_allocator = _global.default_allocator; const C = _global.C; const std = @import("std"); const options = @import("../options.zig"); const logger = @import("../logger.zig"); const cache = @import("../cache.zig"); const js_ast = @import("../js_ast.zig"); const js_lexer = @import("../js_lexer.zig"); // Heuristic: you probably don't have 100 of these // Probably like 5-10 // Array iteration is faster and deterministically ordered in that case. const PathsMap = std.StringArrayHashMap([]string); pub const TSConfigJSON = struct { abs_path: string, // The absolute path of "compilerOptions.baseUrl" base_url: string = "", // This is used if "Paths" is non-nil. It's equal to "BaseURL" except if // "BaseURL" is missing, in which case it is as if "BaseURL" was ".". This // is to implement the "paths without baseUrl" feature from TypeScript 4.1. // More info: https://github.com/microsoft/TypeScript/issues/31869 base_url_for_paths: string = "", // The verbatim values of "compilerOptions.paths". The keys are patterns to // match and the values are arrays of fallback paths to search. Each key and // each fallback path can optionally have a single "*" wildcard character. // If both the key and the value have a wildcard, the substring matched by // the wildcard is substituted into the fallback path. The keys represent // module-style path names and the fallback paths are relative to the // "baseUrl" value in the "tsconfig.json" file. paths: PathsMap, jsx: options.JSX.Pragma = options.JSX.Pragma{}, has_jsxFactory: bool = false, has_jsxFragmentFactory: bool = false, has_jsxImportSource: bool = false, use_define_for_class_fields: ?bool = null, preserve_imports_not_used_as_values: bool = false, pub fn hasBaseURL(tsconfig: *const TSConfigJSON) bool { return tsconfig.base_url.len > 0; } pub const ImportsNotUsedAsValue = enum { preserve, err, remove, invalid, pub const List = std.ComptimeStringMap(ImportsNotUsedAsValue, .{ .{ "preserve", ImportsNotUsedAsValue.preserve }, .{ "error", ImportsNotUsedAsValue.err }, .{ "remove", ImportsNotUsedAsValue.remove }, }); }; pub fn mergeJSX(this: *const TSConfigJSON, current: options.JSX.Pragma) options.JSX.Pragma { var out = current; if (this.has_jsxFactory) { out.factory = this.jsx.factory; } if (this.has_jsxFragmentFactory) { out.fragment = this.jsx.fragment; } if (this.has_jsxImportSource) { out.import_source = this.jsx.import_source; } return out; } pub fn parse( allocator: std.mem.Allocator, log: *logger.Log, source: logger.Source, json_cache: *cache.Json, is_jsx_development: bool, ) anyerror!?*TSConfigJSON { // Unfortunately "tsconfig.json" isn't actually JSON. It's some other // format that appears to be defined by the implementation details of the // TypeScript compiler. // // Attempt to parse it anyway by modifying the JSON parser, but just for // these particular files. This is likely not a completely accurate // emulation of what the TypeScript compiler does (e.g. string escape // behavior may also be different). const json: js_ast.Expr = (json_cache.parseTSConfig(log, source, allocator) catch null) orelse return null; var result: TSConfigJSON = TSConfigJSON{ .abs_path = source.key_path.text, .paths = PathsMap.init(allocator) }; errdefer allocator.free(result.paths); if (json.asProperty("extends")) |extends_value| { if (!source.path.isNodeModule()) { log.addWarning(&source, extends_value.loc, "\"extends\" is not implemented yet") catch unreachable; } // if ((extends_value.expr.asString(allocator) catch null)) |str| { // if (extends(str, source.rangeOfString(extends_value.loc))) |base| { // result.jsx = base.jsx; // result.base_url_for_paths = base.base_url_for_paths; // result.use_define_for_class_fields = base.use_define_for_class_fields; // result.preserve_imports_not_used_as_values = base.preserve_imports_not_used_as_values; // // https://github.com/microsoft/TypeScript/issues/14527#issuecomment-284948808 // result.paths = base.paths; // } // } } var has_base_url = false; // Parse "compilerOptions" if (json.asProperty("compilerOptions")) |compiler_opts| { // Parse "baseUrl" if (compiler_opts.expr.asProperty("baseUrl")) |base_url_prop| { if ((base_url_prop.expr.asString(allocator))) |base_url| { result.base_url = base_url; has_base_url = true; } } // Parse "jsxFactory" if (compiler_opts.expr.asProperty("jsxFactory")) |jsx_prop| { if (jsx_prop.expr.asString(allocator)) |str| { result.jsx.factory = try parseMemberExpressionForJSX(log, &source, jsx_prop.loc, str, allocator); result.has_jsxFactory = true; } } // Parse "jsxFragmentFactory" if (compiler_opts.expr.asProperty("jsxFragmentFactory")) |jsx_prop| { if (jsx_prop.expr.asString(allocator)) |str| { result.jsx.fragment = try parseMemberExpressionForJSX(log, &source, jsx_prop.loc, str, allocator); result.has_jsxFragmentFactory = true; } } // Parse "jsxImportSource" if (compiler_opts.expr.asProperty("jsxImportSource")) |jsx_prop| { if (jsx_prop.expr.asString(allocator)) |str| { if (is_jsx_development) { result.jsx.import_source = std.fmt.allocPrint(allocator, "{s}/jsx-dev-runtime", .{str}) catch unreachable; } else { result.jsx.import_source = std.fmt.allocPrint(allocator, "{s}/jsx-runtime", .{str}) catch unreachable; } result.jsx.package_name = options.JSX.Pragma.parsePackageName(str); result.has_jsxImportSource = true; } } // Parse "useDefineForClassFields" if (compiler_opts.expr.asProperty("useDefineForClassFields")) |use_define_value_prop| { if (use_define_value_prop.expr.asBool()) |val| { result.use_define_for_class_fields = val; } } // Parse "importsNotUsedAsValues" if (compiler_opts.expr.asProperty("importsNotUsedAsValues")) |jsx_prop| { // This should never allocate since it will be utf8 if ((jsx_prop.expr.asString(allocator))) |str| { switch (ImportsNotUsedAsValue.List.get(str) orelse ImportsNotUsedAsValue.invalid) { .preserve, .err => { result.preserve_imports_not_used_as_values = true; }, .remove => {}, else => { log.addRangeWarningFmt(&source, source.rangeOfString(jsx_prop.loc), allocator, "Invalid value \"{s}\" for \"importsNotUsedAsValues\"", .{str}) catch {}; }, } } } // Parse "paths" if (compiler_opts.expr.asProperty("paths")) |paths_prop| { switch (paths_prop.expr.data) { .e_object => { var paths = paths_prop.expr.data.e_object; result.base_url_for_paths = if (result.base_url.len > 0) result.base_url else "."; result.paths = PathsMap.init(allocator); for (paths.properties.slice()) |property| { const key_prop = property.key orelse continue; const key = (key_prop.asString(allocator)) orelse continue; if (!TSConfigJSON.isValidTSConfigPathPattern(key, log, &source, key_prop.loc, allocator)) { continue; } const value_prop = property.value orelse continue; // The "paths" field is an object which maps a pattern to an // array of remapping patterns to try, in priority order. See // the documentation for examples of how this is used: // https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping. // // One particular example: // // { // "compilerOptions": { // "baseUrl": "projectRoot", // "paths": { // "*": [ // "*", // "generated/*" // ] // } // } // } // // Matching "folder1/file2" should first check "projectRoot/folder1/file2" // and then, if that didn't work, also check "projectRoot/generated/folder1/file2". switch (value_prop.data) { .e_array => { const array = value_prop.data.e_array.slice(); if (array.len > 0) { var values = allocator.alloc(string, array.len) catch unreachable; errdefer allocator.free(values); var count: usize = 0; for (array) |expr| { if ((expr.asString(allocator))) |str| { if (TSConfigJSON.isValidTSConfigPathPattern( str, log, &source, expr.loc, allocator, ) and (has_base_url or TSConfigJSON.isValidTSConfigPathNoBaseURLPattern( str, log, &source, allocator, expr.loc, ))) { values[count] = str; count += 1; } } } if (count > 0) { result.paths.put( key, values[0..count], ) catch unreachable; } } }, else => { log.addRangeWarningFmt( &source, source.rangeOfString(key_prop.loc), allocator, "Substitutions for pattern \"{s}\" should be an array", .{key}, ) catch {}; }, } } }, else => {}, } } } if (Environment.isDebug and has_base_url) { std.debug.assert(result.base_url.len > 0); } var _result = allocator.create(TSConfigJSON) catch unreachable; _result.* = result; if (Environment.isDebug and has_base_url) { std.debug.assert(_result.base_url.len > 0); } return _result; } pub fn isValidTSConfigPathPattern(text: string, log: *logger.Log, source: *const logger.Source, loc: logger.Loc, allocator: std.mem.Allocator) bool { var found_asterisk = false; for (text) |c| { if (c == '*') { if (found_asterisk) { const r = source.rangeOfString(loc); log.addRangeWarningFmt(source, r, allocator, "Invalid pattern \"{s}\", must have at most one \"*\" character", .{text}) catch {}; return false; } found_asterisk = true; } } return true; } pub fn parseMemberExpressionForJSX(log: *logger.Log, source: *const logger.Source, loc: logger.Loc, text: string, allocator: std.mem.Allocator) ![]string { if (text.len == 0) { return &([_]string{}); } const parts_count = std.mem.count(u8, text, "."); const parts = allocator.alloc(string, parts_count) catch unreachable; var iter = std.mem.tokenize(u8, text, "."); var i: usize = 0; while (iter.next()) |part| { if (!js_lexer.isIdentifier(part)) { const warn = source.rangeOfString(loc); log.addRangeWarningFmt(source, warn, allocator, "Invalid JSX member expression: \"{s}\"", .{part}) catch {}; return &([_]string{}); } parts[i] = part; i += 1; } return parts; } pub fn isSlash(c: u8) bool { return c == '/' or c == '\\'; } pub fn isValidTSConfigPathNoBaseURLPattern(text: string, log: *logger.Log, source: *const logger.Source, allocator: std.mem.Allocator, loc: logger.Loc) bool { var c0: u8 = 0; var c1: u8 = 0; var c2: u8 = 0; const n = text.len; switch (n) { 0 => { return false; }, // Relative "." or ".." 1 => { return text[0] == '.'; }, // "..", ".\", "./" 2 => { return text[0] == '.' and (text[1] == '.' or text[1] == '\\' or text[1] == '/'); }, else => { c0 = text[0]; c1 = text[1]; c2 = text[2]; }, } // Relative "./" or "../" or ".\\" or "..\\" if (c0 == '.' and (TSConfigJSON.isSlash(c1) or (c1 == '.' and TSConfigJSON.isSlash(c2)))) { return true; } // Absolute DOS "c:/" or "c:\\" if (c1 == ':' and TSConfigJSON.isSlash(c2)) { switch (c0) { 'a'...'z', 'A'...'Z' => { return true; }, else => {}, } } const r = source.rangeOfString(loc); log.addRangeWarningFmt(source, r, allocator, "Non-relative path \"{s}\" is not allowed when \"baseUrl\" is not set (did you forget a leading \"./\"?)", .{text}) catch {}; return false; } };