usingnamespace @import("../global.zig");
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");
const alloc = @import("../alloc.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.getObject();
result.base_url_for_paths = if (result.base_url.len > 0) result.base_url else ".";
result.paths = PathsMap.init(allocator);
for (paths.properties) |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.getArray();
if (array.items.len > 0) {
var values = allocator.alloc(string, array.items.len) catch unreachable;
errdefer allocator.free(values);
var count: usize = 0;
for (array.items) |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 (isDebug and has_base_url) {
std.debug.assert(result.base_url.len > 0);
}
var _result = allocator.create(TSConfigJSON) catch unreachable;
_result.* = result;
if (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, i| {
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;
}
};
test "tsconfig.json" {
try alloc.setup(default_allocator);
}
progress-log'>progress-log
Unnamed repository; edit this file 'description' to name the repository. | |