Skip to content

fix(compartment-mapper,bundle-source): async parser support#3186

Merged
boneskull merged 2 commits into
masterfrom
boneskull/async-parser
May 29, 2026
Merged

fix(compartment-mapper,bundle-source): async parser support#3186
boneskull merged 2 commits into
masterfrom
boneskull/async-parser

Conversation

@boneskull

@boneskull boneskull commented Apr 16, 2026

Copy link
Copy Markdown
Member

This fixes the half-implemented type defs and support for async parsers by introducing AsyncParserImplementation.

We needed sync-and-async-specific parse-mappers in map-parser.js and some type guards.

A sync-parser-specific SyncParserForLanguage is introduced as well.

Bonus: added some tests for import() in ESM to @endo/compartment-mapper.

@boneskull

boneskull commented Apr 16, 2026

Copy link
Copy Markdown
Member Author

📚 Pull Request Stack


Managed by gh-stack

@changeset-bot

changeset-bot Bot commented Apr 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d5be9d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@endo/compartment-mapper Patch
@endo/bundle-source Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@boneskull boneskull force-pushed the boneskull/async-parser branch from f81a724 to 1463114 Compare April 16, 2026 02:04
@boneskull boneskull self-assigned this Apr 16, 2026
@boneskull boneskull added bug Something isn't working lavamoat labels Apr 16, 2026
@boneskull boneskull force-pushed the boneskull/async-parser branch 2 times, most recently from d46967c to 302f011 Compare April 23, 2026 01:42
@boneskull boneskull force-pushed the boneskull/async-parser branch 2 times, most recently from 404d032 to a423cb4 Compare April 30, 2026 01:37
@boneskull boneskull force-pushed the boneskull/async-parser branch 3 times, most recently from d47070d to 6e65a53 Compare May 7, 2026 17:38
@boneskull boneskull force-pushed the boneskull/async-parser branch from 6e65a53 to cef6137 Compare May 12, 2026 23:27
@boneskull boneskull requested review from Copilot and naugtur May 19, 2026 23:11

Copilot AI left a comment

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.

Pull request overview

This PR completes and formalizes async-parser support in @endo/compartment-mapper and @endo/bundle-source, splitting sync vs async parser implementations/types and updating the parser-mapping pipeline accordingly. It also adds a fixture-based test covering import() in ESM for compartment-mapper.

Changes:

  • Introduces AsyncParserImplementation (and related type updates) to properly model async parsers alongside sync parsers.
  • Refactors map-parser.js to generate distinct sync vs async extension parsers and adds type guards to select the correct pipeline.
  • Adds a new ESM dynamic import() fixture and AVA test in @endo/compartment-mapper.

Reviewed changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/compartment-mapper/test/fixtures-dynamic-import-esm/node_modules/app/package.json Adds ESM fixture package metadata for dynamic import test.
packages/compartment-mapper/test/fixtures-dynamic-import-esm/node_modules/app/index.js Adds ESM entry that uses import() dynamically.
packages/compartment-mapper/test/fixtures-dynamic-import-esm/node_modules/app/foo.js Adds simple default export for the dynamic import target.
packages/compartment-mapper/test/dynamic-import-esm.test.js Adds AVA test exercising ESM dynamic import via scaffold.
packages/compartment-mapper/src/types/internal.ts Narrows internal operator type export surface while preserving unions.
packages/compartment-mapper/src/types/external.ts Defines AsyncParserImplementation, tightens sync parser typing, and adds SyncParserForLanguage.
packages/compartment-mapper/src/map-parser.js Splits sync/async parser generation and adds shared language resolution + guards.
packages/compartment-mapper/src/link.js Simplifies access to heuristicImports without unnecessary cast.
packages/compartment-mapper/src/import-hook.js Adds a parse-fn type guard and uses it to enforce sync-only importNow paths.
packages/bundle-source/src/endo.js Updates TypeScript-related parsers to be explicitly async parser implementations.
.changeset/fuzzy-hornets-shout.md Declares patch releases for affected packages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/compartment-mapper/src/map-parser.js Outdated
Comment thread packages/compartment-mapper/src/map-parser.js Outdated
@boneskull boneskull force-pushed the boneskull/async-parser branch 5 times, most recently from 8975d6a to 8101f56 Compare May 21, 2026 21:43
Comment on lines 28 to 38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(should) reuse these from commons

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

can be another PR

) {
return languageForModuleSpecifier[specifier];
}
return languageForExtension[extension] || extension;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(untrusted input) result of resolveLanguage is trusted later on as if it was an internal key, but it's input from untrusted source. index.toString or index.__proto__ are possible filenames

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

index--the Record of transform functions--is also untrusted input, so I'm not sure what you're proposing we do.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Totally different category. Extension is untrusted input for the user of Endo, it's coming from untrusted packages. We need to either use a fixed listy of what is available and throw early here, or very carefully handle the return value to not match prototype fields. The former would be my choice

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Where is it being used as if it is trusted? has omits unowned properties.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm pretty sure this is not a problem in practice, but I've added some extra defense anyway:

const resolveLanguage = (
  specifier,
  location,
  languageForExtension,
  languageForModuleSpecifier,
) => {
  const extension = parseExtension(location);
  if (
    !extensionImpliesLanguage(extension) &&
    has(languageForModuleSpecifier, specifier)
  ) {
    return languageForModuleSpecifier[specifier];
  }
  // new check
  if (has(languageForExtension, extension)) {
    return languageForExtension[extension];
  }
  return extension;
};

So resolveLanguage cannot return anything untoward on languageForExtension. Further:

const validateLanguageForExtension = (
  languageForExtension,
  parserForLanguage,
) => {
  const languageForExtensionEntries = [];
  const problems = [];
  for (const [extension, language] of entries(languageForExtension)) {
    if (has(parserForLanguage, language)) {
      languageForExtensionEntries.push([extension, language]);
    } else {
      problems.push(`${q(language)} for extension ${q(extension)}`);
    }
  }
  if (problems.length > 0) {
    throw Error(`No parser available for language: ${problems.join(', ')}`);
  }
  // null-proto object
  return assign(create(null), fromEntries(languageForExtensionEntries));
};

Now languageForExtension shouldn't even have such properties.

Comment thread packages/compartment-mapper/src/map-parser.js
Comment thread packages/compartment-mapper/src/map-parser.js Outdated
Comment thread packages/compartment-mapper/src/map-parser.js Outdated
Comment thread packages/compartment-mapper/src/map-parser.js Outdated
* @param {*} options
* @returns {Generator<ReturnType<ModuleTransform> | ReturnType<SyncModuleTransform>, ParseResult | Promise<ParseResult>, any>}
*/
function* getParserGenerator(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why put it on a trampoline if it only exists in an async implementation's scope? I doubt it's more efficient that way. Wait, there's 2 of these. they're functionally identical but the other one is typed to optionally return a promise, and that seems wrong (either wrong types or missing yield to remove the promise involvement)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it's intentional...

@naugtur

naugtur commented May 22, 2026

Copy link
Copy Markdown
Member

This is where I'd put things: 46f1fe4
(warning - pseudocode in types)

@boneskull boneskull force-pushed the boneskull/async-parser branch from 8101f56 to fe28002 Compare May 22, 2026 21:47
@boneskull

Copy link
Copy Markdown
Member Author

This is where I'd put things: 46f1fe4
(warning - pseudocode in types)

Types are the problem here, though.

The problem is typing getParserGenerator properly; we need a getParserGenerator that returns a sync parser if we expect a sync parser, and an async parser or sync parser if we don't. And that depends on whether or not we hand it off to the sync or async trampoline. One way to ensure this is to provide a narrowly-typed getParserGenerator based on whether we expect it to return a sync parser or not necessarily—and that's most easily done with a factory function; now we have a factory for sync and a factory for async.

These two factories could be combined into a single polymorphic factory function w/ overloads and inner conditionals, but that looks like adding even more complexity to me.

So that type-fantasy matches reality, I threw in a guard or two to check we're actually working with sync parsers when we expect to be.

@boneskull

Copy link
Copy Markdown
Member Author

(The alternative is just fudging the types and crossing fingers)

@naugtur

naugtur commented May 23, 2026

Copy link
Copy Markdown
Member

Trampolines are in use here to make it possible for code to be reused between sync and async. Having two copies of the same exact function just to hang different types off of defeats the purpose.

The types for the generator are only consumed by trampolines and don't matter for correctness of what we do here.

If there is an unavoidable divergence in the two implementations, removed have to be removed

@boneskull

Copy link
Copy Markdown
Member Author

@naugtur

Trampolines are in use here to make it possible for code to be reused between sync and async. Having two copies of the same exact function just to hang different types off of defeats the purpose.

I see what you're saying here. I'll take another look at it.

@boneskull boneskull force-pushed the boneskull/async-parser branch from a63baeb to 8ef1ce3 Compare May 25, 2026 22:14
@boneskull

Copy link
Copy Markdown
Member Author

@naugtur OK, I've reverted most of my changes to map-parser.js and added a type guard. The types are still not where I'd like them to be, but there's little we could do other than add a type assertion:

/**
 * @typedef GetSyncParserGeneratorFn
 * @param {ParserGeneratorConfig} config
 * @param {Uint8Array} bytes
 * @param {string} specifier
 * @param {string} location
 * @param {string} packageLocation
 * @param {*} options
 * @returns {Generator<ReturnType<SyncModuleTransform>, ParseResult, any>}
 */

const result = syncTrampoline(
  /** @type {GetSyncParserGeneratorFn} */(getParserGenerator),
  config,
  bytes,
  specifier,
  location,
  packageLocation,
  options,
);

I don't view the above as particularly valuable, so I didn't do it.

@boneskull boneskull force-pushed the boneskull/async-parser branch 2 times, most recently from 9ba12b8 to 5aa2a37 Compare May 26, 2026 02:22
@naugtur

naugtur commented May 27, 2026

Copy link
Copy Markdown
Member

looks good, I need one more pass tomorrow, but no comments yet.

boneskull added 2 commits May 28, 2026 17:41
This fixes the half-implemented type defs and support for async parsers by introducing `AsyncParserImplementation`.

We needed sync-and-async-specific parse-mappers in `map-parser.js` and some type guards.

A sync-parser-specific `SyncParserForLanguage` is introduced as well.
This adds missing tests to assert support of `import()` in ES module sources.

As expected, it does not work within archives.
@boneskull boneskull force-pushed the boneskull/async-parser branch from 5aa2a37 to d5be9d8 Compare May 29, 2026 00:41
@boneskull boneskull enabled auto-merge May 29, 2026 00:42
@boneskull boneskull merged commit c49fb04 into master May 29, 2026
16 of 36 checks passed
@boneskull boneskull deleted the boneskull/async-parser branch May 29, 2026 01:06
@kriskowal kriskowal mentioned this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working lavamoat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants