Skip to content

feat: async import loader for JS via static preload (#476)#820

Open
stephenamar-db wants to merge 2 commits intomasterfrom
lazyimporter
Open

feat: async import loader for JS via static preload (#476)#820
stephenamar-db wants to merge 2 commits intomasterfrom
lazyimporter

Conversation

@stephenamar-db
Copy link
Copy Markdown
Collaborator

Adds Preloader + ImportFinder to discover imports from each file's AST and drive caller-controlled (async) loading before synchronous evaluation. Exposes SjsonnetMain.interpretAsync for the JS build, accepting a Promise-returning loader so callers can use fetch / FileReader / etc.

Co-authored-by: Isaac

@stephenamar-db stephenamar-db linked an issue May 1, 2026 that may be closed by this pull request
@stephenamar-db
Copy link
Copy Markdown
Collaborator Author

@He-Pin

Adds Preloader + ImportFinder to discover imports from each file's AST
and drive caller-controlled (async) loading before synchronous evaluation.
Exposes SjsonnetMain.interpretAsync for the JS build, accepting a
Promise-returning loader so callers can use fetch / FileReader / etc.

Co-authored-by: Isaac
Scala 2.13/2.12 promote "private default argument never used" to an
error; the binary-loader and log defaults in the test helpers were
unused at every call site. Inline them since no test exercises binary
imports through the async path.

Co-authored-by: Isaac
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused on the Scala.js async import semantics from #476. The added targeted tests pass, but I found a few cases where interpretAsync diverges from the existing synchronous interpret behavior.

Left(new ParseError(s"$path: ${traced.msg}", offset = traced.index))
case Parsed.Success((expr, _), _) =>
ImportFinder.collect(expr).foreach { found =>
parentImporter.resolve(path, found.value) match {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should resolve against path.parent(), not path. During evaluation Importer.resolveAndReadOrFail calls resolve(pos.fileScope.currentFile.parent(), ...), so the preloader is passing a different docBase to the JS resolver than synchronous interpret does. A resolver that joins docBase + import name will miss nested imports: dir/a.libsonnet importing b.libsonnet gets looked up under dir/a.libsonnet/b.libsonnet instead of dir/b.libsonnet, leaving the cache incomplete before evaluation.

else Future.sequence(batch.map(loadOne)).flatMap(_ => loop())
}

val result: Future[js.Any] = loop().map { _ =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loop() only preloads imports reachable from the entry file AST. runInterpret still parses extVars/tlaVars lazily via Interpreter.parseVar, and those snippets can contain imports. With the cache-only importer, std.extVar('cfg') where cfg is import 'lib.libsonnet' fails with Couldn't import file unless the root file also imports that path. The async path needs to preload parsed ext/tla snippets as code roots too, or otherwise keep variable parsing from hitting a cache-only importer.

val traced = f.trace()
Left(new ParseError(s"$path: ${traced.msg}", offset = traced.index))
case Parsed.Success((expr, _), _) =>
ImportFinder.collect(expr).foreach { found =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collecting every import node here makes interpretAsync eager in places normal Jsonnet evaluation is lazy. For example, if false then import 'bad.libsonnet' else 1 currently rejects if bad.libsonnet has a parse error, while synchronous interpret returns 1 because that branch is never forced. If static preloading is the intended tradeoff, this semantic difference should be explicit in the JS docs/API contract; otherwise this needs a design that does not parse/load imports from unforced branches.

@He-Pin
Copy link
Copy Markdown
Contributor

He-Pin commented May 2, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JavaScript: Proper way to use an async loader callback

2 participants