runtime-checker 1.0.0
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/Cargo.lock +1972 -0
- package/Cargo.toml +46 -0
- package/LICENSE +29 -0
- package/README.md +26 -0
- package/data/mdn/README.md +30 -0
- package/data/mdn/bun.ron +1084 -0
- package/data/mdn/chrome.ron +7814 -0
- package/data/mdn/deno.ron +1497 -0
- package/data/mdn/firefox.ron +6452 -0
- package/data/mdn/node.ron +1311 -0
- package/data/mdn/safari.ron +6267 -0
- package/data/mdn-bcd.version +1 -0
- package/data/node.ron +3561 -0
- package/npm/bin/native/runtime-checker.exe +0 -0
- package/npm/bin/runtime-checker.js +23 -0
- package/npm/postinstall.js +29 -0
- package/npm/prepare-bin.js +11 -0
- package/package.json +42 -0
- package/src/analyzer.rs +405 -0
- package/src/bin/generate-mdn-data.rs +517 -0
- package/src/cli.rs +71 -0
- package/src/data.rs +266 -0
- package/src/engines.rs +154 -0
- package/src/help.rs +192 -0
- package/src/lib.rs +251 -0
- package/src/main.rs +12 -0
- package/src/report.rs +819 -0
- package/src/scanner.rs +404 -0
- package/src/version.rs +101 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
collections::{BTreeMap, BTreeSet},
|
|
3
|
+
fs,
|
|
4
|
+
io::Read,
|
|
5
|
+
path::{Path, PathBuf},
|
|
6
|
+
str::FromStr,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
use anyhow::{Context, Result};
|
|
10
|
+
use clap::Parser;
|
|
11
|
+
use serde_json::Value;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BCD_VERSION: &str = include_str!("../../data/mdn-bcd.version");
|
|
14
|
+
const DEFAULT_OUTPUT_DIR: &str = "data/mdn";
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Parser)]
|
|
17
|
+
#[command(name = "generate-mdn-data")]
|
|
18
|
+
#[command(about = "Generate runtime RON data from MDN browser-compat-data")]
|
|
19
|
+
struct Args {
|
|
20
|
+
/// Read MDN BCD data.json from disk instead of downloading it.
|
|
21
|
+
#[arg(long)]
|
|
22
|
+
input: Option<PathBuf>,
|
|
23
|
+
|
|
24
|
+
/// MDN @mdn/browser-compat-data version to download.
|
|
25
|
+
#[arg(long)]
|
|
26
|
+
bcd_version: Option<String>,
|
|
27
|
+
|
|
28
|
+
/// Directory where runtime RON files are written.
|
|
29
|
+
#[arg(long, default_value = DEFAULT_OUTPUT_DIR)]
|
|
30
|
+
output_dir: PathBuf,
|
|
31
|
+
|
|
32
|
+
/// Runtime ids to generate: nodejs, deno, bun, safari, chrome, firefox.
|
|
33
|
+
#[arg(
|
|
34
|
+
long,
|
|
35
|
+
value_delimiter = ',',
|
|
36
|
+
default_value = "nodejs,deno,bun,safari,chrome,firefox"
|
|
37
|
+
)]
|
|
38
|
+
runtimes: Vec<String>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn main() -> Result<()> {
|
|
42
|
+
let args = Args::parse();
|
|
43
|
+
run(args)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn run(args: Args) -> Result<()> {
|
|
47
|
+
let bcd_version = args
|
|
48
|
+
.bcd_version
|
|
49
|
+
.unwrap_or_else(|| DEFAULT_BCD_VERSION.trim().to_string());
|
|
50
|
+
let input = load_bcd_json(args.input.as_deref(), &bcd_version)?;
|
|
51
|
+
let bcd: Value = serde_json::from_str(&input).context("failed to parse MDN BCD data.json")?;
|
|
52
|
+
|
|
53
|
+
if let Some(actual) = bcd
|
|
54
|
+
.pointer("/__meta/version")
|
|
55
|
+
.and_then(Value::as_str)
|
|
56
|
+
.filter(|actual| *actual != bcd_version)
|
|
57
|
+
{
|
|
58
|
+
anyhow::bail!("expected BCD {bcd_version}, got {actual}");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fs::create_dir_all(&args.output_dir)
|
|
62
|
+
.with_context(|| format!("failed to create {}", args.output_dir.display()))?;
|
|
63
|
+
|
|
64
|
+
for runtime_id in args.runtimes {
|
|
65
|
+
let target = RuntimeTarget::from_bcd_id(&runtime_id)?;
|
|
66
|
+
let features = extract_features(&bcd, target.bcd_id)?;
|
|
67
|
+
let output =
|
|
68
|
+
render_runtime_ron(target.runtime_name, target.bcd_id, &bcd_version, &features);
|
|
69
|
+
let path = args.output_dir.join(format!("{}.ron", target.runtime_name));
|
|
70
|
+
fs::write(&path, output).with_context(|| format!("failed to write {}", path.display()))?;
|
|
71
|
+
println!(
|
|
72
|
+
"generated {} features for {} from MDN BCD {} -> {}",
|
|
73
|
+
features.len(),
|
|
74
|
+
target.runtime_name,
|
|
75
|
+
bcd_version,
|
|
76
|
+
path.display()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Ok(())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn load_bcd_json(input: Option<&Path>, version: &str) -> Result<String> {
|
|
84
|
+
if let Some(input) = input {
|
|
85
|
+
return fs::read_to_string(input)
|
|
86
|
+
.with_context(|| format!("failed to read {}", input.display()));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let url = format!("https://unpkg.com/@mdn/browser-compat-data@{version}/data.json");
|
|
90
|
+
let response = ureq::get(&url)
|
|
91
|
+
.call()
|
|
92
|
+
.with_context(|| format!("failed to download {url}"))?;
|
|
93
|
+
let mut text = String::new();
|
|
94
|
+
response
|
|
95
|
+
.into_reader()
|
|
96
|
+
.read_to_string(&mut text)
|
|
97
|
+
.context("failed to read downloaded MDN BCD data.json")?;
|
|
98
|
+
Ok(text)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Debug, Clone, Copy)]
|
|
102
|
+
struct RuntimeTarget {
|
|
103
|
+
bcd_id: &'static str,
|
|
104
|
+
runtime_name: &'static str,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl RuntimeTarget {
|
|
108
|
+
fn from_bcd_id(id: &str) -> Result<Self> {
|
|
109
|
+
match id {
|
|
110
|
+
"nodejs" | "node" => Ok(Self {
|
|
111
|
+
bcd_id: "nodejs",
|
|
112
|
+
runtime_name: "node",
|
|
113
|
+
}),
|
|
114
|
+
"deno" => Ok(Self {
|
|
115
|
+
bcd_id: "deno",
|
|
116
|
+
runtime_name: "deno",
|
|
117
|
+
}),
|
|
118
|
+
"bun" => Ok(Self {
|
|
119
|
+
bcd_id: "bun",
|
|
120
|
+
runtime_name: "bun",
|
|
121
|
+
}),
|
|
122
|
+
"safari" => Ok(Self {
|
|
123
|
+
bcd_id: "safari",
|
|
124
|
+
runtime_name: "safari",
|
|
125
|
+
}),
|
|
126
|
+
"chrome" => Ok(Self {
|
|
127
|
+
bcd_id: "chrome",
|
|
128
|
+
runtime_name: "chrome",
|
|
129
|
+
}),
|
|
130
|
+
"firefox" => Ok(Self {
|
|
131
|
+
bcd_id: "firefox",
|
|
132
|
+
runtime_name: "firefox",
|
|
133
|
+
}),
|
|
134
|
+
other => anyhow::bail!("unsupported BCD runtime id `{other}`"),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
140
|
+
struct GeneratedFeature {
|
|
141
|
+
name: String,
|
|
142
|
+
version: Version,
|
|
143
|
+
detect: Vec<GeneratedDetectRule>,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
147
|
+
struct Version {
|
|
148
|
+
major: u64,
|
|
149
|
+
minor: u64,
|
|
150
|
+
patch: u64,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl FromStr for Version {
|
|
154
|
+
type Err = anyhow::Error;
|
|
155
|
+
|
|
156
|
+
fn from_str(value: &str) -> Result<Self> {
|
|
157
|
+
let value = value.trim().trim_start_matches('v');
|
|
158
|
+
if !value
|
|
159
|
+
.chars()
|
|
160
|
+
.next()
|
|
161
|
+
.is_some_and(|first| first.is_ascii_digit())
|
|
162
|
+
{
|
|
163
|
+
anyhow::bail!("version is not numeric: {value}");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let mut parts = value.split('.');
|
|
167
|
+
let major = parse_version_part(parts.next().unwrap_or("0"), value, "major")?;
|
|
168
|
+
let minor = parse_version_part(parts.next().unwrap_or("0"), value, "minor")?;
|
|
169
|
+
let patch = parse_version_part(parts.next().unwrap_or("0"), value, "patch")?;
|
|
170
|
+
Ok(Self {
|
|
171
|
+
major,
|
|
172
|
+
minor,
|
|
173
|
+
patch,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
impl std::fmt::Display for Version {
|
|
179
|
+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
180
|
+
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn parse_version_part(part: &str, full: &str, label: &str) -> Result<u64> {
|
|
185
|
+
if part.is_empty() || !part.chars().all(|ch| ch.is_ascii_digit()) {
|
|
186
|
+
anyhow::bail!("invalid {label} version in `{full}`");
|
|
187
|
+
}
|
|
188
|
+
part.parse()
|
|
189
|
+
.with_context(|| format!("invalid {label} version in `{full}`"))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
193
|
+
enum GeneratedDetectRule {
|
|
194
|
+
Global(String),
|
|
195
|
+
MemberChain(String),
|
|
196
|
+
Property(String),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn extract_features(bcd: &Value, runtime_id: &str) -> Result<Vec<GeneratedFeature>> {
|
|
200
|
+
let mut features = BTreeMap::<String, GeneratedFeature>::new();
|
|
201
|
+
|
|
202
|
+
if let Some(api) = bcd.get("api") {
|
|
203
|
+
walk_compat_tree(
|
|
204
|
+
api,
|
|
205
|
+
&mut Vec::new(),
|
|
206
|
+
SourceKind::Api,
|
|
207
|
+
runtime_id,
|
|
208
|
+
&mut features,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if let Some(builtins) = bcd.pointer("/javascript/builtins") {
|
|
213
|
+
walk_compat_tree(
|
|
214
|
+
builtins,
|
|
215
|
+
&mut Vec::new(),
|
|
216
|
+
SourceKind::JavascriptBuiltin,
|
|
217
|
+
runtime_id,
|
|
218
|
+
&mut features,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Ok(features.into_values().collect())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[derive(Debug, Clone, Copy)]
|
|
226
|
+
enum SourceKind {
|
|
227
|
+
Api,
|
|
228
|
+
JavascriptBuiltin,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn walk_compat_tree(
|
|
232
|
+
value: &Value,
|
|
233
|
+
path: &mut Vec<String>,
|
|
234
|
+
source: SourceKind,
|
|
235
|
+
runtime_id: &str,
|
|
236
|
+
features: &mut BTreeMap<String, GeneratedFeature>,
|
|
237
|
+
) {
|
|
238
|
+
let Some(object) = value.as_object() else {
|
|
239
|
+
return;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if let Some(compat) = object.get("__compat")
|
|
243
|
+
&& let Some(version) = support_version(compat, runtime_id)
|
|
244
|
+
{
|
|
245
|
+
let name = feature_name(path);
|
|
246
|
+
if !name.is_empty() && is_runtime_surface_path(path) {
|
|
247
|
+
let detect = detect_rules(source, path);
|
|
248
|
+
if !detect.is_empty() {
|
|
249
|
+
upsert_feature(
|
|
250
|
+
features,
|
|
251
|
+
GeneratedFeature {
|
|
252
|
+
name,
|
|
253
|
+
version,
|
|
254
|
+
detect,
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (key, child) in object {
|
|
262
|
+
if key == "__compat" {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
path.push(key.clone());
|
|
266
|
+
walk_compat_tree(child, path, source, runtime_id, features);
|
|
267
|
+
path.pop();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn upsert_feature(features: &mut BTreeMap<String, GeneratedFeature>, feature: GeneratedFeature) {
|
|
272
|
+
match features.get(&feature.name) {
|
|
273
|
+
Some(existing) if existing.version <= feature.version => {}
|
|
274
|
+
_ => {
|
|
275
|
+
features.insert(feature.name.clone(), feature);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn support_version(compat: &Value, runtime_id: &str) -> Option<Version> {
|
|
281
|
+
let support = compat.get("support")?.get(runtime_id)?;
|
|
282
|
+
let statements: Vec<&Value> = match support {
|
|
283
|
+
Value::Array(values) => values.iter().collect(),
|
|
284
|
+
value => vec![value],
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
statements
|
|
288
|
+
.into_iter()
|
|
289
|
+
.filter(|statement| !has_runtime_flags(statement))
|
|
290
|
+
.filter(|statement| !has_version_removed(statement))
|
|
291
|
+
.filter_map(|statement| statement.get("version_added")?.as_str())
|
|
292
|
+
.filter_map(|version| version.parse::<Version>().ok())
|
|
293
|
+
.min()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn has_runtime_flags(statement: &Value) -> bool {
|
|
297
|
+
statement
|
|
298
|
+
.get("flags")
|
|
299
|
+
.and_then(Value::as_array)
|
|
300
|
+
.is_some_and(|flags| !flags.is_empty())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn has_version_removed(statement: &Value) -> bool {
|
|
304
|
+
statement
|
|
305
|
+
.get("version_removed")
|
|
306
|
+
.is_some_and(|version| !version.is_null())
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn feature_name(path: &[String]) -> String {
|
|
310
|
+
path.join(".")
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn is_runtime_surface_path(path: &[String]) -> bool {
|
|
314
|
+
if path.is_empty() {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if path.len() > 1 && path.first() == path.last() {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
path.iter()
|
|
323
|
+
.all(|segment| is_runtime_surface_segment(segment))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn is_runtime_surface_segment(segment: &str) -> bool {
|
|
327
|
+
if segment.is_empty() || segment.starts_with("@@") || segment.contains('-') {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if segment.contains('_') {
|
|
332
|
+
return segment
|
|
333
|
+
.chars()
|
|
334
|
+
.all(|ch| ch == '_' || ch.is_ascii_digit() || ch.is_ascii_uppercase());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
segment
|
|
338
|
+
.chars()
|
|
339
|
+
.all(|ch| ch == '$' || ch.is_ascii_alphanumeric())
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fn detect_rules(source: SourceKind, path: &[String]) -> Vec<GeneratedDetectRule> {
|
|
343
|
+
if path.is_empty() {
|
|
344
|
+
return Vec::new();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let name = feature_name(path);
|
|
348
|
+
let mut rules = BTreeSet::new();
|
|
349
|
+
|
|
350
|
+
if path.len() == 1 {
|
|
351
|
+
rules.insert(GeneratedDetectRule::Global(name.clone()));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
rules.insert(GeneratedDetectRule::MemberChain(name));
|
|
355
|
+
|
|
356
|
+
if matches!(source, SourceKind::JavascriptBuiltin)
|
|
357
|
+
&& path.len() >= 2
|
|
358
|
+
&& let Some(property) = path
|
|
359
|
+
.last()
|
|
360
|
+
.filter(|property| is_detectable_property(property))
|
|
361
|
+
{
|
|
362
|
+
rules.insert(GeneratedDetectRule::Property(property.clone()));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
rules.into_iter().collect()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn is_detectable_property(property: &str) -> bool {
|
|
369
|
+
!property.starts_with("@@")
|
|
370
|
+
&& !property.starts_with("__")
|
|
371
|
+
&& property
|
|
372
|
+
.chars()
|
|
373
|
+
.next()
|
|
374
|
+
.is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn render_runtime_ron(
|
|
378
|
+
runtime_name: &str,
|
|
379
|
+
bcd_id: &str,
|
|
380
|
+
bcd_version: &str,
|
|
381
|
+
features: &[GeneratedFeature],
|
|
382
|
+
) -> String {
|
|
383
|
+
let mut output = String::new();
|
|
384
|
+
output.push_str(&format!(
|
|
385
|
+
"//! Generated from MDN @mdn/browser-compat-data {bcd_version} runtime `{bcd_id}`.\n"
|
|
386
|
+
));
|
|
387
|
+
output.push_str(
|
|
388
|
+
"//! Regenerate with: cargo run --bin generate-mdn-data -- --output-dir data/mdn\n",
|
|
389
|
+
);
|
|
390
|
+
output.push_str("(\n");
|
|
391
|
+
output.push_str(" schema: 1,\n");
|
|
392
|
+
output.push_str(&format!(" runtime: {},\n", quote(runtime_name)));
|
|
393
|
+
output.push_str(" features: [\n");
|
|
394
|
+
|
|
395
|
+
for feature in features {
|
|
396
|
+
output.push_str(" (name: ");
|
|
397
|
+
output.push_str("e(&feature.name));
|
|
398
|
+
output.push_str(", version: ");
|
|
399
|
+
output.push_str("e(&feature.version.to_string()));
|
|
400
|
+
output.push_str(", detect: [");
|
|
401
|
+
for (index, rule) in feature.detect.iter().enumerate() {
|
|
402
|
+
if index > 0 {
|
|
403
|
+
output.push_str(", ");
|
|
404
|
+
}
|
|
405
|
+
output.push_str(&render_detect_rule(rule));
|
|
406
|
+
}
|
|
407
|
+
output.push_str("]),\n");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
output.push_str(" ],\n");
|
|
411
|
+
output.push_str(")\n");
|
|
412
|
+
output
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fn render_detect_rule(rule: &GeneratedDetectRule) -> String {
|
|
416
|
+
match rule {
|
|
417
|
+
GeneratedDetectRule::Global(name) => format!("Global({})", quote(name)),
|
|
418
|
+
GeneratedDetectRule::MemberChain(name) => format!("MemberChain({})", quote(name)),
|
|
419
|
+
GeneratedDetectRule::Property(name) => format!("Property({})", quote(name)),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fn quote(value: &str) -> String {
|
|
424
|
+
serde_json::to_string(value).expect("string escaping cannot fail")
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[cfg(test)]
|
|
428
|
+
mod tests {
|
|
429
|
+
use super::*;
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn extracts_runtime_versions_from_bcd_shape() {
|
|
433
|
+
let bcd = serde_json::json!({
|
|
434
|
+
"__meta": { "version": "fixture" },
|
|
435
|
+
"api": {
|
|
436
|
+
"fetch": {
|
|
437
|
+
"__compat": {
|
|
438
|
+
"support": {
|
|
439
|
+
"nodejs": { "version_added": "18.0.0" },
|
|
440
|
+
"deno": { "version_added": "1.0" }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
"javascript": {
|
|
446
|
+
"builtins": {
|
|
447
|
+
"Array": {
|
|
448
|
+
"toSorted": {
|
|
449
|
+
"__compat": {
|
|
450
|
+
"support": {
|
|
451
|
+
"nodejs": { "version_added": "20.0.0" }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"options_parameter": {
|
|
456
|
+
"__compat": {
|
|
457
|
+
"support": {
|
|
458
|
+
"nodejs": { "version_added": "21.0.0" }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
"flagged": {
|
|
463
|
+
"__compat": {
|
|
464
|
+
"support": {
|
|
465
|
+
"nodejs": {
|
|
466
|
+
"version_added": "21.0.0",
|
|
467
|
+
"flags": [{ "name": "flag" }]
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
"Number": {
|
|
474
|
+
"MAX_SAFE_INTEGER": {
|
|
475
|
+
"__compat": {
|
|
476
|
+
"support": {
|
|
477
|
+
"nodejs": { "version_added": "0.12.0" }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
"Temporal": {
|
|
483
|
+
"__compat": {
|
|
484
|
+
"support": {
|
|
485
|
+
"nodejs": { "version_added": "26.0.0" }
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
let features = extract_features(&bcd, "nodejs").unwrap();
|
|
494
|
+
let names: Vec<_> = features
|
|
495
|
+
.iter()
|
|
496
|
+
.map(|feature| feature.name.as_str())
|
|
497
|
+
.collect();
|
|
498
|
+
|
|
499
|
+
assert!(names.contains(&"fetch"));
|
|
500
|
+
assert!(names.contains(&"Array.toSorted"));
|
|
501
|
+
assert!(names.contains(&"Number.MAX_SAFE_INTEGER"));
|
|
502
|
+
assert!(names.contains(&"Temporal"));
|
|
503
|
+
assert!(!names.contains(&"Array.options_parameter"));
|
|
504
|
+
assert!(!names.contains(&"Array.flagged"));
|
|
505
|
+
|
|
506
|
+
let to_sorted = features
|
|
507
|
+
.iter()
|
|
508
|
+
.find(|feature| feature.name == "Array.toSorted")
|
|
509
|
+
.unwrap();
|
|
510
|
+
assert_eq!(to_sorted.version.to_string(), "20.0.0");
|
|
511
|
+
assert!(
|
|
512
|
+
to_sorted
|
|
513
|
+
.detect
|
|
514
|
+
.contains(&GeneratedDetectRule::Property("toSorted".to_string()))
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
package/src/cli.rs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use std::path::PathBuf;
|
|
2
|
+
|
|
3
|
+
use clap::{Parser, ValueEnum};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
6
|
+
pub enum RuntimeKind {
|
|
7
|
+
All,
|
|
8
|
+
Node,
|
|
9
|
+
Deno,
|
|
10
|
+
Bun,
|
|
11
|
+
Safari,
|
|
12
|
+
Chrome,
|
|
13
|
+
Firefox,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ALL_RUNTIMES: &[RuntimeKind] = &[
|
|
17
|
+
RuntimeKind::Node,
|
|
18
|
+
RuntimeKind::Deno,
|
|
19
|
+
RuntimeKind::Bun,
|
|
20
|
+
RuntimeKind::Safari,
|
|
21
|
+
RuntimeKind::Chrome,
|
|
22
|
+
RuntimeKind::Firefox,
|
|
23
|
+
];
|
|
24
|
+
const NODE_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Node];
|
|
25
|
+
const DENO_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Deno];
|
|
26
|
+
const BUN_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Bun];
|
|
27
|
+
const SAFARI_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Safari];
|
|
28
|
+
const CHROME_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Chrome];
|
|
29
|
+
const FIREFOX_RUNTIME: &[RuntimeKind] = &[RuntimeKind::Firefox];
|
|
30
|
+
|
|
31
|
+
impl RuntimeKind {
|
|
32
|
+
pub fn targets(self) -> &'static [RuntimeKind] {
|
|
33
|
+
match self {
|
|
34
|
+
Self::All => ALL_RUNTIMES,
|
|
35
|
+
Self::Node => NODE_RUNTIME,
|
|
36
|
+
Self::Deno => DENO_RUNTIME,
|
|
37
|
+
Self::Bun => BUN_RUNTIME,
|
|
38
|
+
Self::Safari => SAFARI_RUNTIME,
|
|
39
|
+
Self::Chrome => CHROME_RUNTIME,
|
|
40
|
+
Self::Firefox => FIREFOX_RUNTIME,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Parser)]
|
|
46
|
+
#[command(name = "runtime-checker")]
|
|
47
|
+
#[command(about = "Detect the minimum runtime version required by a codebase")]
|
|
48
|
+
pub struct Cli {
|
|
49
|
+
/// Directory to scan.
|
|
50
|
+
pub dir: PathBuf,
|
|
51
|
+
|
|
52
|
+
/// Use text matching only. This is intentionally less reliable.
|
|
53
|
+
#[arg(long)]
|
|
54
|
+
pub fast: bool,
|
|
55
|
+
|
|
56
|
+
/// Runtime compatibility target.
|
|
57
|
+
#[arg(long, value_enum, default_value_t = RuntimeKind::All)]
|
|
58
|
+
pub runtime: RuntimeKind,
|
|
59
|
+
|
|
60
|
+
/// Print only the summary panel.
|
|
61
|
+
#[arg(long)]
|
|
62
|
+
pub summary: bool,
|
|
63
|
+
|
|
64
|
+
/// Print every detection for one feature instead of grouped summaries.
|
|
65
|
+
#[arg(long, value_name = "FEATURE")]
|
|
66
|
+
pub inspect: Option<String>,
|
|
67
|
+
|
|
68
|
+
/// Update package.json engines.node when it is missing or too low.
|
|
69
|
+
#[arg(long)]
|
|
70
|
+
pub fix: bool,
|
|
71
|
+
}
|