Scripts: $.line and $.stack give the line of the file the code is in

$.line and $.stack counted lines in the run's expanded source. In code
that comes after an #include they gave lines that match no file, and
this included code in a file $.evalFile ran and code a named engine
kept from an earlier run.

Both now map each frame through the run's source maps, as uncaught
errors already were. $.stack writes each frame as name@file:line,
outermost first, with the run's name as the file of a script given as
source. Code that eval or Function compiled keeps the line of the
source they compiled, in [eval]; code whose place a named engine no
longer keeps is at line 0 of [unknown]. The 64-frame limit, [global]
and [anonymous] are unchanged.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 06:19:54 -08:00
parent e8c7421a3d
commit 4091f110b1
11 changed files with 182 additions and 8 deletions
+35
View File
@@ -822,6 +822,34 @@ impl Host {
}
}
/// The file and line that line `line` of the source with tag `tag` came from, as `$.line` and
/// `$.stack` give them: the run's script, a file it included or a file `$.evalFile` gave, in
/// this run or in an earlier run of its named engine, with the run's name standing for a script
/// given as text. Code that `eval` or `Function` compiled has no map and keeps its own line,
/// named `[eval]`; code whose map is gone, such as one the named engine let go to stay within
/// [`KEPT_MAP_BYTES`], is at line 0 of `[unknown]`.
fn source_place(&self, tag: u32, line: u32) -> (String, u32) {
if tag == vectorcraft_js::UNKNOWN_SOURCE {
return ("[eval]".to_string(), line);
}
let named = |map: &include::SourceMap, script: Option<&Arc<str>>| {
let (index, file, line, _) = map.locate(line, 1);
match script {
Some(name) if index == 0 && file.is_empty() => (name.to_string(), line),
_ => (file.to_string(), line),
}
};
let script = self.script_tag.get();
let placed = match tag.checked_sub(script).filter(|_| script != vectorcraft_js::UNKNOWN_SOURCE) {
Some(0) => self.script_map.try_borrow().ok().map(|m| named(&m, Some(&self.report_name))),
Some(n) => self.eval_maps.try_borrow().ok().and_then(|maps| maps.get(n as usize - 1).map(|m| named(m, None))),
None => {
self.kept_maps.as_ref().and_then(|k| k.try_borrow().ok()).and_then(|k| k.get(tag).map(|kept| named(&kept.map, kept.script.as_ref())))
}
};
placed.unwrap_or_else(|| ("[unknown]".to_string(), 0))
}
/// Adds a line to the run's console and, during a slice, to the session's.
fn line(&self, entry: console::Entry, cut: bool) {
let line = console::ConsoleLine { run: self.run, name: self.name.clone(), entry };
@@ -1858,6 +1886,13 @@ impl Session {
Ok(expanded) => {
*host.script_map.borrow_mut() = expanded.map;
realm.set_strict_writes(expanded.strict == Some(true));
if fresh {
let l = link.clone();
realm.set_source_locator(Rc::new(move |tag, line| match current(&l) {
Ok(h) => h.source_place(tag, line),
Err(_) => ("[unknown]".to_string(), 0),
}));
}
(if fresh { realm.add_script_extensions() } else { Ok(()) })
.and_then(|()| if fresh { realm.define_host_object("vc", vc_object(&link)) } else { Ok(()) })
.and_then(|()| if fresh { realm.define_host_object("$", dollar_object(&link, info, &localization)) } else { Ok(()) })
+2 -2
View File
@@ -63,7 +63,7 @@ fn each_member_has_its_value() {
("$.memCache", json!(0)),
("$.os", json!(os)),
("$.screens.length", json!(0)),
("$.stack", json!("[global]:1\n")),
("$.stack", json!("[global]@t.js:1\n")),
("$.strict", json!(false)),
("$.version", json!(version)),
("typeof $.getenv + typeof $.setenv + typeof $.sleep", json!("functionfunctionfunction")),
@@ -165,7 +165,7 @@ fn getenv_reads_only_its_names_and_setenv_changes_only_the_run() {
fn error_line_and_stack_follow_the_running_code() {
let mut s = Session::new();
let source = "try { null.x; } catch (e) {}\nfunction where() {\n return [$.line, $.stack, $.error instanceof TypeError];\n}\nvc.log(where());";
assert_eq!(logged(&mut s, source)[0], json!([3, "[global]:5\nwhere:3\n", true]));
assert_eq!(logged(&mut s, source)[0], json!([3, "[global]@t.js:5\nwhere@t.js:3\n", true]));
assert_eq!(value(&mut s, "$.error"), json!(null), "each run starts without one");
}
+38
View File
@@ -230,6 +230,44 @@ fn an_engine_keeps_the_newest_places_within_its_budget() {
assert_eq!((d_place.0, d_place.1, d_place.2), (None, 0, 0), "a map larger than the budget is not kept");
}
/// `$.line` and `$.stack` in code an earlier run of a named engine compiled give the line of that
/// run's file: its script, or a file it included; a script given as text is named by that run's
/// name. Code `eval` and `Function` compiled keeps its own line, in `[eval]`, and code whose place
/// the engine did not keep is at line 0 of `[unknown]`.
#[test]
fn the_line_and_the_stack_of_code_an_earlier_run_kept_are_at_that_runs_files() {
use super::script_include::short_stack;
let base: PathBuf = std::env::temp_dir().join(format!("vc-script-engines-line-stack-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("inc.jsxinc"), "\n\nfunction fromInclude() {\n return [$.line, $.stack];\n}").unwrap();
let def = base.join("def.jsx");
std::fs::write(&def, "#targetengine e\nvar pad = 1;\n#include \"inc.jsxinc\"\nfunction fromMain() {\n return fromInclude();\n}").unwrap();
let mut s = Session::new();
s.execute("script.run", &json!({"path": def.to_string_lossy()})).unwrap();
run(&mut s, "#targetengine e\n\nfunction fromText() {\n return $.stack;\n}").unwrap();
let call = |s: &mut Session, code: &str| {
let r = s.execute("script.run", &json!({"source": format!("#targetengine e\n\n{code}"), "name": "caller.js"}));
r.unwrap_or_else(|e| panic!("{code}: {e}"))["log"][0].clone()
};
let r = call(&mut s, "vc.log(fromMain());");
assert_eq!(r[0], 4, "{r}");
assert_eq!(short_stack(&r[1]), "[global]@caller.js:3\nfromMain@def.jsx:5\nfromInclude@inc.jsxinc:4\n");
assert_eq!(short_stack(&call(&mut s, "vc.log(fromText());")), "[global]@caller.js:3\nfromText@t.js:4\n");
assert_eq!(call(&mut s, "vc.log(eval('\\n\\n$.line'));"), 3);
assert_eq!(call(&mut s, "vc.log(eval('\\n$.stack'));"), "[global]@caller.js:3\n[global]@[eval]:2\n");
let r = call(&mut s, "vc.log(new Function('\\nreturn $.stack;')());");
assert_eq!(r, "[global]@caller.js:3\n[anonymous]@[eval]:4\n", "the line of the function source `Function` compiles");
// A map larger than the budget is not kept.
TEST_KEPT_MAP_BYTES.with(|b| b.set(Some(1)));
let lost = run(&mut s, "#targetengine e\n\n\nfunction lost() {\n return [$.line, $.stack];\n}");
let r = lost.map(|_| call(&mut s, "vc.log(lost());"));
TEST_KEPT_MAP_BYTES.with(|b| b.set(None));
let r = r.unwrap();
assert_eq!(r, json!([0, "[global]@caller.js:3\nlost@[unknown]:0\n"]));
let _ = std::fs::remove_dir_all(base);
}
/// A run of a named engine whose script does not compile defines no code, so it keeps no place
/// and pushes out none of the places kept before. An included file without a preprocessor line is
/// first parsed with the script it joins, so its syntax error ends the run as it starts.
+43
View File
@@ -454,6 +454,49 @@ fn script_included_error_has_local_file_and_line() {
let _ = std::fs::remove_dir_all(base);
}
/// `$.stack`'s text with each frame's file cut to its name: `function@name:line` a line.
pub(crate) fn short_stack(stack: &Value) -> String {
let stack = stack.as_str().unwrap_or_else(|| panic!("a stack: {stack}"));
stack
.lines()
.map(|frame| match (frame.split_once('@'), frame.rsplit_once(':')) {
(Some((name, _)), Some((place, line))) => {
let file = place.split_once('@').map_or(place, |(_, f)| f).rsplit(['/', '\\']).next().unwrap_or_default();
format!("{name}@{file}:{line}\n")
}
_ => format!("{frame}\n"),
})
.collect()
}
/// `$.line` and `$.stack` give the line of the file the running code is in: the script, a file it
/// included (and a file that one included) or a file `$.evalFile` gave and the files it included,
/// whatever lines come before it in the run's expanded source.
#[test]
fn script_line_and_stack_are_at_the_line_of_each_file() {
let base = folder("line-stack");
write(
&base.join("main/lib.jsxinc"),
"\nfunction libWhere() {\n return [$.line, $.stack];\n}\nvar libLine = $.line;\n#include \"deep.jsxinc\"\n",
);
write(&base.join("main/deep.jsxinc"), "\n\nvar deepLine = $.line;\nfunction deepStack() {\n return $.stack;\n}");
write(&base.join("main/sub/ev.jsx"), "#include \"inc.jsxinc\"\nfunction evWhere() {\n return incWhere();\n}\nevWhere();");
write(&base.join("main/sub/inc.jsxinc"), "\n\nfunction incWhere() {\n return [$.line, $.stack];\n}");
let text = "var a = 1;\nvar b = 2;\n#include \"lib.jsxinc\"\nvar mainLine = $.line;\nvc.log([libLine, deepLine, mainLine]);\n\
vc.log(libWhere());\nvc.log(deepStack());\nvc.log($.evalFile('sub/ev.jsx'));";
let log = run(&base, text).unwrap()["log"].clone();
assert_eq!(log[0], json!([5, 3, 4]), "{log}");
assert_eq!(log[1][0], 3, "{log}");
assert_eq!(short_stack(&log[1][1]), "[global]@main.jsx:6\nlibWhere@lib.jsxinc:3\n");
assert_eq!(short_stack(&log[2]), "[global]@main.jsx:7\ndeepStack@deep.jsxinc:5\n");
assert_eq!(log[3][0], 4, "{log}");
assert_eq!(short_stack(&log[3][1]), "[global]@main.jsx:8\n[global]@ev.jsx:5\nevWhere@ev.jsx:3\nincWhere@inc.jsxinc:4\n");
// The file a frame names is the one `$.fileName` gives while that file runs.
let log = run(&base, "vc.log($.stack.slice(9, -3) === $.fileName);").unwrap()["log"].clone();
assert_eq!(log, json!([true]));
let _ = std::fs::remove_dir_all(base);
}
/// Pumps the run in the app until it ends or waits at a dialog (`None`).
fn pump(s: &mut Session) -> Option<Result<Value>> {
for _ in 0..10_000 {
+21 -4
View File
@@ -388,18 +388,33 @@ fn last_error(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<V
}
fn line(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
let line = interp.script_frames().last().map_or(0, |(code, pc, _)| code.line_column(pc.saturating_sub(1)).0);
let line = interp.script_frames().last().map_or(0, |(code, pc, _)| place(interp.locator.as_ref(), code, pc).1);
Ok(Value::Number(f64::from(line)))
}
/// Where the instruction before `pc` in `code` came from: the file the realm's locator names
/// (`None` without one) and the line there, or the line in the compiled source without a locator.
fn place(locator: Option<&crate::realm::SourceLocator>, code: &Code, pc: usize) -> (Option<String>, u32) {
let line = code.line_column(pc.saturating_sub(1)).0;
match locator {
Some(locate) => {
let (file, line) = locate(code.source.tag, line);
(Some(file), line)
}
None => (None, line),
}
}
/// The most frames `$.stack` lists, and the most characters of a function's name it shows.
const STACK_FRAMES: usize = 64;
const STACK_NAME_UNITS: usize = 64;
/// The script's frames, outermost first, one a line: the function's name and the line it is at.
/// The script's frames, outermost first, one a line: the function's name and the line it is at,
/// as `name:line`, or as `name@file:line` with the file the realm's locator names.
fn stack(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
let frames: Vec<(Rc<Code>, usize, bool)> = interp.script_frames().map(|(c, pc, f)| (c.clone(), pc, f)).collect();
let skip = frames.len().saturating_sub(STACK_FRAMES);
let locator = interp.locator.clone();
let mut text = String::new();
if skip > 0 {
text.push_str("…\n");
@@ -407,8 +422,10 @@ fn stack(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value>
for (code, pc, function) in frames.iter().skip(skip) {
interp.use_fuel()?;
let name = if *function { function_name(code) } else { "[global]".to_string() };
let line = code.line_column(pc.saturating_sub(1)).0;
text.push_str(&format!("{name}:{line}\n"));
match place(locator.as_ref(), code, *pc) {
(Some(file), line) => text.push_str(&format!("{name}@{file}:{line}\n")),
(None, line) => text.push_str(&format!("{name}:{line}\n")),
}
}
let mut out = Builder::new();
out.push_str(interp, &text)?;
+26
View File
@@ -219,6 +219,32 @@ fn script_items_read_the_global_object_the_last_error_the_line_and_the_stack() {
assert!(thrown(&mut r, "'use strict'; h.line = 1").starts_with("TypeError"), "the items cannot be assigned");
}
#[test]
fn the_line_and_the_stack_give_the_place_the_source_locator_maps_each_frame_to() {
let mut r = realm();
let items = [("line", ScriptItem::Line), ("stack", ScriptItem::Stack)].into_iter().map(|(n, i)| (n.to_string(), HostItem::Script(i))).collect();
r.define_host_object("h", items).unwrap();
let asked: Rc<RefCell<Vec<(u32, u32)>>> = Rc::default();
let seen = asked.clone();
r.set_source_locator(Rc::new(move |tag, line| {
seen.borrow_mut().push((tag, line));
let file = if tag == UNKNOWN_SOURCE { "[eval]".to_string() } else { format!("f{tag}") };
(file, line + 100)
}));
let source = "function inner() {\n return h.stack;\n}\n\ninner()";
let stack = text(&mut r, source);
let tag = r.script_tag();
assert_eq!(stack, format!("[global]@f{tag}:105\ninner@f{tag}:102\n"));
assert_eq!(*asked.borrow(), [(tag, 5), (tag, 2)], "each frame's tag and its line in the compiled source");
assert_eq!(r.run("\n\n h.line").unwrap(), JsValue::Number(103.0));
let tag = r.script_tag();
let stack = text(&mut r, "\neval('\\n\\nh.stack')");
assert_eq!(stack, format!("[global]@f{}:102\n[global]@[eval]:103\n", tag + 1), "code eval compiles has no tag of its own");
let deep = "function d(n) { return n ? d(n - 1) : h.stack; } var s = d(100).split('\\n'); s.length + ',' + s[0] + ',' + s[1]";
let tag = r.script_tag() + 1;
assert_eq!(text(&mut r, deep), format!("66,…,d@f{tag}:101"), "the frames past 64 are still left out");
}
#[test]
fn a_host_property_reads_its_getter_and_gives_assignments_to_its_setter() {
let mut r = Realm::new();
+3
View File
@@ -232,6 +232,8 @@ pub(crate) struct Interp {
pub(crate) evaluated: u32,
/// The tag of the source the last run's error was raised in.
pub(crate) error_tag: u32,
/// Where `$.line` and `$.stack` find the file and line of a compiled line.
pub(crate) locator: Option<crate::realm::SourceLocator>,
/// The mark of the host error ([`crate::HostError::Marked`]) whose exception or limit is
/// propagating.
pub(crate) thrown_mark: Option<u32>,
@@ -469,6 +471,7 @@ impl Interp {
source_tag: crate::realm::UNKNOWN_SOURCE,
evaluated: 0,
error_tag: crate::realm::UNKNOWN_SOURCE,
locator: None,
thrown_mark: None,
finally_marks: Vec::new(),
finally_sites: Vec::new(),
+1 -1
View File
@@ -83,7 +83,7 @@ pub use error::{SyntaxError, TOO_DEEP, TOO_LONG};
pub use host::{Arg, Data, DataLimits, HostError, HostFunction, HostItem, ScriptItem};
pub use lexer::{Goal, Lexer};
pub use number::to_string as number_to_string;
pub use realm::{Interrupt, JsValue, LimitKind, Limits, MAX_UNBROKEN_FUEL, Progress, Realm, RunError, RunErrorKind, UNKNOWN_SOURCE};
pub use realm::{Interrupt, JsValue, LimitKind, Limits, MAX_UNBROKEN_FUEL, Progress, Realm, RunError, RunErrorKind, SourceLocator, UNKNOWN_SOURCE};
pub use token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind};
/// How a script is read.
+11
View File
@@ -63,6 +63,10 @@ pub enum LimitKind {
/// evaluator's: code that `eval` or `Function` compiled, or an error with no place.
pub const UNKNOWN_SOURCE: u32 = u32::MAX;
/// The file and line that line `line` of the source with tag `tag` ([`Realm::error_tag`]) came
/// from, as `$.line` and `$.stack` give them ([`Realm::set_source_locator`]).
pub type SourceLocator = std::rc::Rc<dyn Fn(u32, u32) -> (String, u32)>;
/// The source tag after `tag`. Once the tags are used up, every later source gets
/// [`UNKNOWN_SOURCE`], so no two sources share a tag.
pub(crate) fn next_tag(tag: u32) -> u32 {
@@ -395,6 +399,13 @@ impl Realm {
self.interp.interrupt = interrupt.0.clone();
}
/// Has `$.line` give the line `locate` maps the running line to, and `$.stack` give each
/// frame's line that way, after the file `locate` names: `function@file:line`. Without a
/// locator they give the line of the compiled source, and `$.stack` names no file.
pub fn set_source_locator(&mut self, locate: SourceLocator) {
self.interp.locator = Some(locate);
}
/// Which source the last error's `line` and `column` are in: 0 for the script last given to
/// [`Realm::run`] or [`Realm::start`], `n` for the `n`th source a
/// [`crate::HostItem::Evaluator`] gave since then, and [`UNKNOWN_SOURCE`] otherwise, which
+1 -1
View File
@@ -2018,7 +2018,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available:
| `global` | the global object |
| `hiresTimer` | the microseconds since the run last read it, or since the run started; `0` on the web |
| `level`, `memCache` | `0`; setting `memCache` throws |
| `line`, `stack` | the line of the code that reads it; the running code, outermost first, one a line as `name:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name), at most 64 lines |
| `line`, `stack` | the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine); the running code, outermost first, one a line as `name@file:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name; the file as `$.fileName` gives it, or the run's name for a script given as `source`), at most 64 lines. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`; code whose place a named engine no longer keeps is at line `0` of `[unknown]` |
| `locale` | the interface's language as a locale such as `de` or `pt_BR` (`en` when the interface follows the system); a script can set `ll` or `ll_RR`, and `null` or `""` sets the interface's again |
| `localize` | `false`; while `true`, `String(object)` of a localization object gives its text for `$.locale` |
| `os` | `Windows`, `macOS`, `Linux`, `FreeBSD`, `Unix` or `Web`, without a version |
+1
View File
@@ -783,6 +783,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, and `$.fileName` and `$.includePath` give the file being run and the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`).
- Scripts: `#targetengine name` (and `//@targetengine name`) runs the script in a named engine that lasts for the session, and a later script that names it sees the globals and functions earlier ones left ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/)); `$.engineName` gives the name ([the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: a script without the line and `#targetengine main` get a new engine each run, the line comes once in the script itself before its code, a session keeps 8 named engines with 64 MiB of heap each, an error or a stop keeps the globals the run set while the documents go back, kept host functions act for the run that calls them, `script.engines` lists the engines and `script.engine.drop` drops one, and the web build has none. `crates/engine/src/tests/script_engines.rs`.
- Scripts: a runtime error is shown at the line that was running ([the help page](https://extendscript.docsforadobe.dev/extendscript-toolkit/debugging-in-the-toolkit/)). VectorCraft's own rules: an error in code an earlier run of a named engine compiled, such as a function it defined, is reported at that code's file, line and column (the earlier run's script, a file it included or a file its `$.evalFile` ran), while the report and its console line keep the calling run's name; a script given as text is named by its run's name, and each engine keeps these places for 8 MiB of source maps, the oldest going first. An error in code whose place is gone, or in code that `eval` or `Function` compiled, is reported without a line and column, and the code still runs. `crates/engine/src/tests/script_engines.rs`.
- Scripts: `$.line` is the line of the running script, counting from 1, and `$.stack` is the current stack trace ([the help page](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: both count lines in the file the code is in (the script, a file it included, a file `$.evalFile` ran, or the file of an earlier run of the named engine), and `$.stack` writes each frame as `name@file:line`, outermost first; code that `eval` or `Function` compiled keeps its own line in `[eval]`, and code whose place the engine no longer keeps is at line 0 of `[unknown]`. `crates/engine/src/tests/script_include.rs`, `crates/engine/src/tests/script_engines.rs`, `crates/js/src/extension_tests.rs`.
- Scripts: `File` and `Folder` ([File object](https://extendscript.docsforadobe.dev/file-system-access/file-object/), [Folder object](https://extendscript.docsforadobe.dev/file-system-access/folder-object/)) name files and folders by platform or URI path, with `name`, `fsName`, `absoluteURI`, `relativeURI`, `parent`, `Folder.current` and `encode`/`decode`, and read them: `exists`, `length`, the dates, `getFiles` with a mask or a function, and `open('r')` with `read`, `readln`, `readch`, `seek`, `tell` and `close`, decoding by byte order mark, `encoding`, UTF-8 or CP1252. VectorCraft's own rules: a script reads only in its own folder and below (and inside the automation read roots when they are set); any other path and a network or device path are refused before anything is looked up, and a link that leads out is refused as well (`false` or `null`, with `error` set); a file is read whole up to 8 MiB and a run reads up to 32 MiB; writing, the file dialogs and `execute` are not available yet. `crates/engine/src/tests/script_io.rs`.
- Scripts: `#strict on` (and `//@strict on`) runs the script with the checks the [`$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/) help page gives for `$.strict`: an assignment to a read-only property throws a `TypeError`, and so does a new property on an object that takes none. VectorCraft's own rules: a getter-only property counts as read-only, the objects that take no new properties are the non-extensible ones (`Object.preventExtensions`, `seal`, `freeze`, and the frozen `vc` and `$` objects), the last `#strict` line of the script and its includes sets the whole run, a file `$.evalFile` runs keeps its own line's checks for its code, and the rest of ES5 strict mode stays with `'use strict'`. Without the line, runs are unchanged. `crates/engine/src/tests/script_include.rs` (`strict_on_*`, `strict_mode_in_eval_file_is_scoped`), `crates/js/src/interp_tests.rs` (`strict_writes_are_separate_from_es5_strict`).
- Scripts folder: File › Scripts lists the `.js` and `.jsx` files of the `Scripts` folder next to the preferences, each named after its file without the extension, with each subfolder as a submenu, above Check Script… and Other Script… (https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/install-and-run-scripts.html). VectorCraft's own rules: the folder is read again when the window becomes active and when the File menu opens (in the macOS menu bar, when any of its menus opens, and the new listing shows the next time the menu opens), links are not followed, and one read stops at 2,048 entries, 512 scripts, 128 subfolders, 4 levels or 250 ms. `script.listInstalled` returns the listing. `crates/engine/src/tests/script_folder.rs`, `crates/ui-egui/src/tests/scripts_menu.rs`.