@ttsc/lint 0.18.4 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +159 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +231 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +206 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- package/src/structures/rules/index.ts +3 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
// Config and custom-stats discovery for the browserslist port
|
|
2
|
+
// (`polyfill_browserslist.go`): a transliteration of browserslist 4.28.6
|
|
3
|
+
// `node.js` — `loadConfig`, `findConfig(File)`, `parseConfig`,
|
|
4
|
+
// `parsePackage`, `pickEnv`, `getStat`, and `normalizeStats` — minus the
|
|
5
|
+
// process-level caches (every lookup re-reads the filesystem so long-lived
|
|
6
|
+
// hosts never serve stale config) and minus `extends` / regional loaders,
|
|
7
|
+
// which need a JS runtime or data ttsc does not embed.
|
|
8
|
+
package linthost
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"fmt"
|
|
13
|
+
"os"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"regexp"
|
|
16
|
+
"sort"
|
|
17
|
+
"strings"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// browserslistSectionValue is one config section: either a query array or the
|
|
21
|
+
// raw string form package.json allows (`"browserslist": "query"`), whose JS
|
|
22
|
+
// falsiness when empty decides fallbacks.
|
|
23
|
+
type browserslistSectionValue struct {
|
|
24
|
+
queries []string
|
|
25
|
+
isString bool
|
|
26
|
+
str string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// browserslistPackageConfig is a parsed config file: env sections plus the
|
|
30
|
+
// `invalid` marker for truthy non-config `browserslist` values, which
|
|
31
|
+
// upstream carries through and fails on only when the config is used.
|
|
32
|
+
type browserslistPackageConfig struct {
|
|
33
|
+
sections map[string]browserslistSectionValue
|
|
34
|
+
invalid bool
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var browserslistSectionPattern = regexp.MustCompile(`^\s*\[(.+)]\s*$`)
|
|
38
|
+
var browserslistCommentPattern = regexp.MustCompile(`#[^\n]*`)
|
|
39
|
+
|
|
40
|
+
// browserslistLoadConfig mirrors loadConfig: the BROWSERSLIST /
|
|
41
|
+
// BROWSERSLIST_CONFIG environment overrides, then config discovery from the
|
|
42
|
+
// path. `found` reports a JS-truthy config (an empty query array is still
|
|
43
|
+
// truthy, and resolves to an empty browser list).
|
|
44
|
+
func browserslistLoadConfig(opts browserslistOpts) (queries []string, found bool, err error) {
|
|
45
|
+
if value := os.Getenv("BROWSERSLIST"); value != "" {
|
|
46
|
+
return []string{value}, true, nil
|
|
47
|
+
}
|
|
48
|
+
if file := os.Getenv("BROWSERSLIST_CONFIG"); file != "" {
|
|
49
|
+
config, parseErr := browserslistParsePackageOrReadConfig(file)
|
|
50
|
+
if parseErr != nil {
|
|
51
|
+
return nil, false, parseErr
|
|
52
|
+
}
|
|
53
|
+
return browserslistPickEnv(config, opts)
|
|
54
|
+
}
|
|
55
|
+
if opts.path != "" {
|
|
56
|
+
config, findErr := browserslistFindConfig(opts.path)
|
|
57
|
+
if findErr != nil {
|
|
58
|
+
return nil, false, findErr
|
|
59
|
+
}
|
|
60
|
+
return browserslistPickEnv(config, opts)
|
|
61
|
+
}
|
|
62
|
+
return nil, false, nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// browserslistPickEnv mirrors pickEnv over the parsed sections, using JS
|
|
66
|
+
// truthiness: a section holding an empty string falls through to `defaults`,
|
|
67
|
+
// and an empty-string result reads as "no config".
|
|
68
|
+
func browserslistPickEnv(config *browserslistPackageConfig, opts browserslistOpts) ([]string, bool, error) {
|
|
69
|
+
if config == nil {
|
|
70
|
+
return nil, false, nil
|
|
71
|
+
}
|
|
72
|
+
if config.invalid {
|
|
73
|
+
return nil, false, newBrowserslistError(
|
|
74
|
+
"Browserslist config should be a string or an array of strings with browser queries")
|
|
75
|
+
}
|
|
76
|
+
name := opts.env
|
|
77
|
+
if name == "" {
|
|
78
|
+
name = os.Getenv("BROWSERSLIST_ENV")
|
|
79
|
+
}
|
|
80
|
+
if name == "" {
|
|
81
|
+
name = os.Getenv("NODE_ENV")
|
|
82
|
+
}
|
|
83
|
+
if name == "" {
|
|
84
|
+
name = "production"
|
|
85
|
+
}
|
|
86
|
+
section, ok := config.sections[name]
|
|
87
|
+
if !ok || browserslistSectionFalsy(section) {
|
|
88
|
+
section, ok = config.sections["defaults"]
|
|
89
|
+
if !ok || browserslistSectionFalsy(section) {
|
|
90
|
+
return nil, false, nil
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if section.isString {
|
|
94
|
+
return []string{section.str}, true, nil
|
|
95
|
+
}
|
|
96
|
+
if section.queries == nil {
|
|
97
|
+
return []string{}, true, nil
|
|
98
|
+
}
|
|
99
|
+
return section.queries, true, nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// browserslistSectionFalsy reports the JS falsiness of a section value: only
|
|
103
|
+
// the empty string is falsy (an empty array is truthy).
|
|
104
|
+
func browserslistSectionFalsy(section browserslistSectionValue) bool {
|
|
105
|
+
return section.isString && section.str == ""
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func browserslistParsePackageOrReadConfig(file string) (*browserslistPackageConfig, error) {
|
|
109
|
+
if filepath.Base(file) == "package.json" {
|
|
110
|
+
return browserslistParsePackage(file)
|
|
111
|
+
}
|
|
112
|
+
return browserslistReadConfig(file)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// browserslistParsePackage mirrors parsePackage. Config-shape problems come
|
|
116
|
+
// back as *browserslistConfigError (findConfigFile re-throws those) while
|
|
117
|
+
// unreadable / unparsable JSON comes back as a generic error (findConfigFile
|
|
118
|
+
// treats the directory as config-free, like upstream's warn-and-ignore).
|
|
119
|
+
func browserslistParsePackage(file string) (*browserslistPackageConfig, error) {
|
|
120
|
+
text, err := os.ReadFile(file)
|
|
121
|
+
if err != nil {
|
|
122
|
+
return nil, fmt.Errorf("could not read %s: %w", file, err)
|
|
123
|
+
}
|
|
124
|
+
body := strings.TrimPrefix(string(text), "\uFEFF")
|
|
125
|
+
if strings.Contains(body, `"browserslist"`) {
|
|
126
|
+
var parsed struct {
|
|
127
|
+
Browserslist json.RawMessage `json:"browserslist"`
|
|
128
|
+
}
|
|
129
|
+
if jsonErr := json.Unmarshal([]byte(body), &parsed); jsonErr != nil {
|
|
130
|
+
return nil, fmt.Errorf("could not parse %s: %w", file, jsonErr)
|
|
131
|
+
}
|
|
132
|
+
return browserslistNormalizePackageValue(parsed.Browserslist)
|
|
133
|
+
}
|
|
134
|
+
if strings.Contains(body, `"browserlist"`) {
|
|
135
|
+
var parsed struct {
|
|
136
|
+
Browserlist json.RawMessage `json:"browserlist"`
|
|
137
|
+
}
|
|
138
|
+
if jsonErr := json.Unmarshal([]byte(body), &parsed); jsonErr != nil {
|
|
139
|
+
return nil, fmt.Errorf("could not parse %s: %w", file, jsonErr)
|
|
140
|
+
}
|
|
141
|
+
if browserslistJSONTruthy(parsed.Browserlist) {
|
|
142
|
+
return nil, newBrowserslistError(
|
|
143
|
+
"`browserlist` key instead of `browserslist` in %s", file)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return nil, nil
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// browserslistNormalizePackageValue turns the raw `browserslist` package.json
|
|
150
|
+
// value into sections, mirroring the string/array wrapping and the `check()`
|
|
151
|
+
// validation. Truthy scalars become the deferred `invalid` config; falsy ones
|
|
152
|
+
// read as "no config here".
|
|
153
|
+
func browserslistNormalizePackageValue(raw json.RawMessage) (*browserslistPackageConfig, error) {
|
|
154
|
+
trimmed := strings.TrimSpace(string(raw))
|
|
155
|
+
if trimmed == "" || trimmed == "null" {
|
|
156
|
+
return nil, nil
|
|
157
|
+
}
|
|
158
|
+
switch trimmed[0] {
|
|
159
|
+
case '"':
|
|
160
|
+
var value string
|
|
161
|
+
if err := json.Unmarshal(raw, &value); err != nil {
|
|
162
|
+
return nil, fmt.Errorf("invalid browserslist string: %w", err)
|
|
163
|
+
}
|
|
164
|
+
return &browserslistPackageConfig{
|
|
165
|
+
sections: map[string]browserslistSectionValue{
|
|
166
|
+
"defaults": {isString: true, str: value},
|
|
167
|
+
},
|
|
168
|
+
}, nil
|
|
169
|
+
case '[':
|
|
170
|
+
queries, err := browserslistDecodeQueryArray(raw)
|
|
171
|
+
if err != nil {
|
|
172
|
+
return nil, err
|
|
173
|
+
}
|
|
174
|
+
return &browserslistPackageConfig{
|
|
175
|
+
sections: map[string]browserslistSectionValue{"defaults": {queries: queries}},
|
|
176
|
+
}, nil
|
|
177
|
+
case '{':
|
|
178
|
+
var sections map[string]json.RawMessage
|
|
179
|
+
if err := json.Unmarshal(raw, §ions); err != nil {
|
|
180
|
+
return nil, fmt.Errorf("invalid browserslist sections: %w", err)
|
|
181
|
+
}
|
|
182
|
+
config := &browserslistPackageConfig{sections: map[string]browserslistSectionValue{}}
|
|
183
|
+
for name, value := range sections {
|
|
184
|
+
valueTrimmed := strings.TrimSpace(string(value))
|
|
185
|
+
if strings.HasPrefix(valueTrimmed, `"`) {
|
|
186
|
+
var str string
|
|
187
|
+
if err := json.Unmarshal(value, &str); err != nil {
|
|
188
|
+
return nil, fmt.Errorf("invalid browserslist section: %w", err)
|
|
189
|
+
}
|
|
190
|
+
config.sections[name] = browserslistSectionValue{isString: true, str: str}
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
queries, err := browserslistDecodeQueryArray(value)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return nil, err
|
|
196
|
+
}
|
|
197
|
+
config.sections[name] = browserslistSectionValue{queries: queries}
|
|
198
|
+
}
|
|
199
|
+
return config, nil
|
|
200
|
+
default:
|
|
201
|
+
// Scalar: JS keeps falsy values as "no config" and carries truthy ones
|
|
202
|
+
// until `pickEnv` / query checking fails on them.
|
|
203
|
+
if trimmed == "false" || trimmed == "0" {
|
|
204
|
+
return nil, nil
|
|
205
|
+
}
|
|
206
|
+
return &browserslistPackageConfig{invalid: true}, nil
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// browserslistDecodeQueryArray mirrors check(): a section must be an array
|
|
211
|
+
// whose every element is a string (the string form is handled by callers).
|
|
212
|
+
func browserslistDecodeQueryArray(raw json.RawMessage) ([]string, error) {
|
|
213
|
+
var elements []json.RawMessage
|
|
214
|
+
if !strings.HasPrefix(strings.TrimSpace(string(raw)), "[") {
|
|
215
|
+
return nil, newBrowserslistError(
|
|
216
|
+
"Browserslist config should be a string or an array of strings with browser queries")
|
|
217
|
+
}
|
|
218
|
+
if err := json.Unmarshal(raw, &elements); err != nil {
|
|
219
|
+
return nil, newBrowserslistError(
|
|
220
|
+
"Browserslist config should be a string or an array of strings with browser queries")
|
|
221
|
+
}
|
|
222
|
+
queries := make([]string, 0, len(elements))
|
|
223
|
+
for _, element := range elements {
|
|
224
|
+
var query string
|
|
225
|
+
if err := json.Unmarshal(element, &query); err != nil {
|
|
226
|
+
return nil, newBrowserslistError(
|
|
227
|
+
"Browserslist config should be a string or an array of strings with browser queries")
|
|
228
|
+
}
|
|
229
|
+
queries = append(queries, query)
|
|
230
|
+
}
|
|
231
|
+
return queries, nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func browserslistJSONTruthy(raw json.RawMessage) bool {
|
|
235
|
+
trimmed := strings.TrimSpace(string(raw))
|
|
236
|
+
switch trimmed {
|
|
237
|
+
case "", "null", "false", "0", `""`:
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// browserslistParseConfig mirrors parseConfig for `browserslist` /
|
|
244
|
+
// `.browserslistrc` files: strip `#` comments, split lines on newlines AND
|
|
245
|
+
// commas, and route queries into `[section]` groups.
|
|
246
|
+
func browserslistParseConfig(text string) (*browserslistPackageConfig, error) {
|
|
247
|
+
config := &browserslistPackageConfig{
|
|
248
|
+
sections: map[string]browserslistSectionValue{"defaults": {queries: []string{}}},
|
|
249
|
+
}
|
|
250
|
+
sections := []string{"defaults"}
|
|
251
|
+
body := browserslistCommentPattern.ReplaceAllString(text, "")
|
|
252
|
+
for _, line := range strings.FieldsFunc(body, func(r rune) bool { return r == '\n' || r == ',' }) {
|
|
253
|
+
line = strings.TrimSpace(line)
|
|
254
|
+
if line == "" {
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
if match := browserslistSectionPattern.FindStringSubmatch(line); match != nil {
|
|
258
|
+
sections = strings.Split(strings.TrimSpace(match[1]), " ")
|
|
259
|
+
for _, section := range sections {
|
|
260
|
+
if _, duplicate := config.sections[section]; duplicate {
|
|
261
|
+
return nil, newBrowserslistError(
|
|
262
|
+
"Duplicate section %s in Browserslist config", section)
|
|
263
|
+
}
|
|
264
|
+
config.sections[section] = browserslistSectionValue{queries: []string{}}
|
|
265
|
+
}
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
for _, section := range sections {
|
|
269
|
+
value := config.sections[section]
|
|
270
|
+
value.queries = append(value.queries, line)
|
|
271
|
+
config.sections[section] = value
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return config, nil
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func browserslistReadConfig(file string) (*browserslistPackageConfig, error) {
|
|
278
|
+
if !browserslistIsFile(file) {
|
|
279
|
+
return nil, newBrowserslistError("Can't read %s config", file)
|
|
280
|
+
}
|
|
281
|
+
text, err := os.ReadFile(file)
|
|
282
|
+
if err != nil {
|
|
283
|
+
return nil, newBrowserslistError("Can't read %s config", file)
|
|
284
|
+
}
|
|
285
|
+
return browserslistParseConfig(string(text))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func browserslistIsFile(path string) bool {
|
|
289
|
+
stat, err := os.Stat(path)
|
|
290
|
+
return err == nil && stat.Mode().IsRegular()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func browserslistIsDirectory(path string) bool {
|
|
294
|
+
stat, err := os.Stat(path)
|
|
295
|
+
return err == nil && stat.IsDir()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// browserslistPathInRoot mirrors pathInRoot: BROWSERSLIST_ROOT_PATH bounds
|
|
299
|
+
// upward walks.
|
|
300
|
+
func browserslistPathInRoot(p string) bool {
|
|
301
|
+
root := os.Getenv("BROWSERSLIST_ROOT_PATH")
|
|
302
|
+
if root == "" {
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
rootAbs, err := filepath.Abs(root)
|
|
306
|
+
if err != nil {
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
relative, err := filepath.Rel(rootAbs, p)
|
|
310
|
+
if err != nil {
|
|
311
|
+
// Different volumes: node's path.relative returns the target itself,
|
|
312
|
+
// which never starts with "..".
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
return !strings.HasPrefix(relative, "..")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// browserslistEachParent mirrors eachParent without the process cache: walk
|
|
319
|
+
// from `file` to the filesystem root, invoking the callback on directories.
|
|
320
|
+
func browserslistEachParent(file string, callback func(dir string) (string, bool, error)) (string, error) {
|
|
321
|
+
loc, err := filepath.Abs(file)
|
|
322
|
+
if err != nil {
|
|
323
|
+
return "", nil
|
|
324
|
+
}
|
|
325
|
+
loc = filepath.Clean(loc)
|
|
326
|
+
for {
|
|
327
|
+
if !browserslistPathInRoot(loc) {
|
|
328
|
+
return "", nil
|
|
329
|
+
}
|
|
330
|
+
if browserslistIsDirectory(loc) {
|
|
331
|
+
result, done, callbackErr := callback(loc)
|
|
332
|
+
if callbackErr != nil {
|
|
333
|
+
return "", callbackErr
|
|
334
|
+
}
|
|
335
|
+
if done {
|
|
336
|
+
return result, nil
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
parent := filepath.Dir(loc)
|
|
340
|
+
if parent == loc {
|
|
341
|
+
return "", nil
|
|
342
|
+
}
|
|
343
|
+
loc = parent
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// browserslistFindConfigFile mirrors findConfigFile, including the
|
|
348
|
+
// conflicting-config errors and the "ignore unparsable package.json" rule.
|
|
349
|
+
func browserslistFindConfigFile(from string) (string, error) {
|
|
350
|
+
return browserslistEachParent(from, func(dir string) (string, bool, error) {
|
|
351
|
+
config := filepath.Join(dir, "browserslist")
|
|
352
|
+
pkg := filepath.Join(dir, "package.json")
|
|
353
|
+
rc := filepath.Join(dir, ".browserslistrc")
|
|
354
|
+
|
|
355
|
+
var pkgBrowserslist *browserslistPackageConfig
|
|
356
|
+
if browserslistIsFile(pkg) {
|
|
357
|
+
parsed, err := browserslistParsePackage(pkg)
|
|
358
|
+
if err != nil {
|
|
359
|
+
if _, isBrowserslistError := err.(*browserslistConfigError); isBrowserslistError {
|
|
360
|
+
return "", false, err
|
|
361
|
+
}
|
|
362
|
+
// Upstream warns and ignores package.json files it cannot parse.
|
|
363
|
+
} else {
|
|
364
|
+
pkgBrowserslist = parsed
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
configIsFile := browserslistIsFile(config)
|
|
369
|
+
rcIsFile := browserslistIsFile(rc)
|
|
370
|
+
switch {
|
|
371
|
+
case configIsFile && pkgBrowserslist != nil:
|
|
372
|
+
return "", false, newBrowserslistError(
|
|
373
|
+
"%s contains both browserslist and package.json with browsers", dir)
|
|
374
|
+
case rcIsFile && pkgBrowserslist != nil:
|
|
375
|
+
return "", false, newBrowserslistError(
|
|
376
|
+
"%s contains both .browserslistrc and package.json with browsers", dir)
|
|
377
|
+
case configIsFile && rcIsFile:
|
|
378
|
+
return "", false, newBrowserslistError(
|
|
379
|
+
"%s contains both .browserslistrc and browserslist", dir)
|
|
380
|
+
case configIsFile:
|
|
381
|
+
return config, true, nil
|
|
382
|
+
case rcIsFile:
|
|
383
|
+
return rc, true, nil
|
|
384
|
+
case pkgBrowserslist != nil:
|
|
385
|
+
return pkg, true, nil
|
|
386
|
+
}
|
|
387
|
+
return "", false, nil
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func browserslistFindConfig(from string) (*browserslistPackageConfig, error) {
|
|
392
|
+
configFile, err := browserslistFindConfigFile(from)
|
|
393
|
+
if err != nil || configFile == "" {
|
|
394
|
+
return nil, err
|
|
395
|
+
}
|
|
396
|
+
return browserslistParsePackageOrReadConfig(configFile)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// --- custom usage stats -------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
// browserslistGetStat mirrors getStat + normalizeStats + fillUsage: locate a
|
|
402
|
+
// browserslist-stats.json (env override or upward walk), then flatten it into
|
|
403
|
+
// "browser version" usage keys preserving JS object iteration order. JSON
|
|
404
|
+
// null usage values are tracked separately because they skip percentage
|
|
405
|
+
// filters yet still occupy coverage slots.
|
|
406
|
+
func browserslistGetStat(opts browserslistOpts) (map[string]float64, []string, map[string]bool, error) {
|
|
407
|
+
statsPath := os.Getenv("BROWSERSLIST_STATS")
|
|
408
|
+
if statsPath == "" && opts.path != "" {
|
|
409
|
+
found, err := browserslistEachParent(opts.path, func(dir string) (string, bool, error) {
|
|
410
|
+
candidate := filepath.Join(dir, "browserslist-stats.json")
|
|
411
|
+
if browserslistIsFile(candidate) {
|
|
412
|
+
return candidate, true, nil
|
|
413
|
+
}
|
|
414
|
+
return "", false, nil
|
|
415
|
+
})
|
|
416
|
+
if err != nil {
|
|
417
|
+
return nil, nil, nil, err
|
|
418
|
+
}
|
|
419
|
+
statsPath = found
|
|
420
|
+
}
|
|
421
|
+
if statsPath == "" {
|
|
422
|
+
return nil, nil, nil, nil
|
|
423
|
+
}
|
|
424
|
+
content, err := os.ReadFile(statsPath)
|
|
425
|
+
if err != nil {
|
|
426
|
+
return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
|
|
427
|
+
}
|
|
428
|
+
root, err := browserslistParseOrderedJSON(content)
|
|
429
|
+
if err != nil {
|
|
430
|
+
return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
|
|
431
|
+
}
|
|
432
|
+
stats, ok := root.(*browserslistOrderedObject)
|
|
433
|
+
if !ok {
|
|
434
|
+
// `'dataByBrowser' in stats` throws on JSON null / scalars upstream.
|
|
435
|
+
return nil, nil, nil, newBrowserslistError("Can't read %s", statsPath)
|
|
436
|
+
}
|
|
437
|
+
if inner, has := stats.get("dataByBrowser"); has {
|
|
438
|
+
innerObject, isObject := inner.(*browserslistOrderedObject)
|
|
439
|
+
if !isObject {
|
|
440
|
+
return nil, nil, nil, nil
|
|
441
|
+
}
|
|
442
|
+
stats = innerObject
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
data := browserslistData()
|
|
446
|
+
values := map[string]float64{}
|
|
447
|
+
var order []string
|
|
448
|
+
nulls := map[string]bool{}
|
|
449
|
+
for _, browser := range stats.keys {
|
|
450
|
+
browserStats, isObject := stats.values[browser].(*browserslistOrderedObject)
|
|
451
|
+
if !isObject {
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
versions := browserStats.keys
|
|
455
|
+
// normalizeStats: a one-version stat for a one-version browser is
|
|
456
|
+
// rekeyed onto the browser's canonical version.
|
|
457
|
+
if len(versions) == 1 {
|
|
458
|
+
if agent, known := data.data[browser]; known && len(agent.versions) == 1 {
|
|
459
|
+
key := browser + " " + agent.versions[0]
|
|
460
|
+
browserslistAppendStat(values, &order, nulls, key, browserStats.values[versions[0]])
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for _, version := range versions {
|
|
465
|
+
key := browser + " " + version
|
|
466
|
+
browserslistAppendStat(values, &order, nulls, key, browserStats.values[version])
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return values, order, nulls, nil
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
func browserslistAppendStat(values map[string]float64, order *[]string, nulls map[string]bool, key string, value interface{}) {
|
|
473
|
+
if _, dup := values[key]; dup {
|
|
474
|
+
// fillUsage overwrites, keeping first key position.
|
|
475
|
+
switch typed := value.(type) {
|
|
476
|
+
case float64:
|
|
477
|
+
values[key] = typed
|
|
478
|
+
delete(nulls, key)
|
|
479
|
+
case nil:
|
|
480
|
+
values[key] = 0
|
|
481
|
+
nulls[key] = true
|
|
482
|
+
}
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
switch typed := value.(type) {
|
|
486
|
+
case float64:
|
|
487
|
+
values[key] = typed
|
|
488
|
+
case nil:
|
|
489
|
+
values[key] = 0
|
|
490
|
+
nulls[key] = true
|
|
491
|
+
default:
|
|
492
|
+
// Non-numeric usage values behave like null in the percentage filters.
|
|
493
|
+
values[key] = 0
|
|
494
|
+
nulls[key] = true
|
|
495
|
+
}
|
|
496
|
+
*order = append(*order, key)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- ordered JSON --------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
// browserslistOrderedObject preserves JS object key semantics for
|
|
502
|
+
// runtime-parsed JSON: canonical array indexes first in ascending order, then
|
|
503
|
+
// the remaining keys in document order.
|
|
504
|
+
type browserslistOrderedObject struct {
|
|
505
|
+
keys []string
|
|
506
|
+
values map[string]interface{}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
func (o *browserslistOrderedObject) get(key string) (interface{}, bool) {
|
|
510
|
+
value, ok := o.values[key]
|
|
511
|
+
return value, ok
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
var browserslistArrayIndexPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)$`)
|
|
515
|
+
|
|
516
|
+
// browserslistParseOrderedJSON decodes JSON keeping object key order.
|
|
517
|
+
func browserslistParseOrderedJSON(content []byte) (interface{}, error) {
|
|
518
|
+
decoder := json.NewDecoder(strings.NewReader(string(content)))
|
|
519
|
+
decoder.UseNumber()
|
|
520
|
+
value, err := browserslistDecodeOrderedValue(decoder)
|
|
521
|
+
if err != nil {
|
|
522
|
+
return nil, err
|
|
523
|
+
}
|
|
524
|
+
if decoder.More() {
|
|
525
|
+
return nil, newBrowserslistError("trailing JSON content")
|
|
526
|
+
}
|
|
527
|
+
return value, nil
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
func browserslistDecodeOrderedValue(decoder *json.Decoder) (interface{}, error) {
|
|
531
|
+
token, err := decoder.Token()
|
|
532
|
+
if err != nil {
|
|
533
|
+
return nil, err
|
|
534
|
+
}
|
|
535
|
+
switch typed := token.(type) {
|
|
536
|
+
case json.Delim:
|
|
537
|
+
switch typed {
|
|
538
|
+
case '{':
|
|
539
|
+
object := &browserslistOrderedObject{values: map[string]interface{}{}}
|
|
540
|
+
for decoder.More() {
|
|
541
|
+
keyToken, keyErr := decoder.Token()
|
|
542
|
+
if keyErr != nil {
|
|
543
|
+
return nil, keyErr
|
|
544
|
+
}
|
|
545
|
+
key := keyToken.(string)
|
|
546
|
+
value, valueErr := browserslistDecodeOrderedValue(decoder)
|
|
547
|
+
if valueErr != nil {
|
|
548
|
+
return nil, valueErr
|
|
549
|
+
}
|
|
550
|
+
if _, dup := object.values[key]; !dup {
|
|
551
|
+
object.keys = append(object.keys, key)
|
|
552
|
+
}
|
|
553
|
+
object.values[key] = value
|
|
554
|
+
}
|
|
555
|
+
if _, err := decoder.Token(); err != nil { // consume '}'
|
|
556
|
+
return nil, err
|
|
557
|
+
}
|
|
558
|
+
browserslistApplyJSKeyOrder(object)
|
|
559
|
+
return object, nil
|
|
560
|
+
case '[':
|
|
561
|
+
var array []interface{}
|
|
562
|
+
for decoder.More() {
|
|
563
|
+
value, valueErr := browserslistDecodeOrderedValue(decoder)
|
|
564
|
+
if valueErr != nil {
|
|
565
|
+
return nil, valueErr
|
|
566
|
+
}
|
|
567
|
+
array = append(array, value)
|
|
568
|
+
}
|
|
569
|
+
if _, err := decoder.Token(); err != nil { // consume ']'
|
|
570
|
+
return nil, err
|
|
571
|
+
}
|
|
572
|
+
return array, nil
|
|
573
|
+
}
|
|
574
|
+
return nil, newBrowserslistError("unexpected JSON delimiter")
|
|
575
|
+
case json.Number:
|
|
576
|
+
parsed, parseErr := typed.Float64()
|
|
577
|
+
if parseErr != nil {
|
|
578
|
+
return nil, parseErr
|
|
579
|
+
}
|
|
580
|
+
return parsed, nil
|
|
581
|
+
default:
|
|
582
|
+
return token, nil // string, bool, nil
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// browserslistApplyJSKeyOrder reorders object keys the way JS property
|
|
587
|
+
// enumeration does: integer-like keys ascending before insertion-ordered
|
|
588
|
+
// string keys.
|
|
589
|
+
func browserslistApplyJSKeyOrder(object *browserslistOrderedObject) {
|
|
590
|
+
var indexKeys []string
|
|
591
|
+
var stringKeys []string
|
|
592
|
+
for _, key := range object.keys {
|
|
593
|
+
if browserslistArrayIndexPattern.MatchString(key) && len(key) <= 10 {
|
|
594
|
+
indexKeys = append(indexKeys, key)
|
|
595
|
+
} else {
|
|
596
|
+
stringKeys = append(stringKeys, key)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
sort.Slice(indexKeys, func(i, j int) bool {
|
|
600
|
+
return jsParseInt(indexKeys[i]) < jsParseInt(indexKeys[j])
|
|
601
|
+
})
|
|
602
|
+
object.keys = append(indexKeys, stringKeys...)
|
|
603
|
+
}
|