Skip to content

Support ESModule natively for config using internal API#6

Open
ghostflyby wants to merge 1 commit intocmsj:mainfrom
ghostflyby:main
Open

Support ESModule natively for config using internal API#6
ghostflyby wants to merge 1 commit intocmsj:mainfrom
ghostflyby:main

Conversation

@ghostflyby
Copy link
Copy Markdown

@ghostflyby ghostflyby commented Dec 28, 2025

details

  • uses JSC internal OBJC APIs to utilize native esmodule support in JSC.
  • taking the place of require
  • introducing a asynchronous evalFromUrl

Some tests just fails with nothing to do with my changes.

demo

2025-12-28.22.37.59.mov

future works

unfortunately, the ESModule only supports ones starting with /, ./ and ../ according to https://github.com/WebKit/WebKit/blob/8615f506db9162ea73173834af9e4b997d92dd93/Source/JavaScriptCore/API/JSAPIGlobalObject.mm#L94

To support custom schemes or modulemap like features for introducing npm, I'll have to use internal cpp based APIs instead of ObjC-based ones.

Copilot AI review requested due to automatic review settings December 28, 2025 14:36
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 introduces native ESModule support to Hammerspoon 2 by leveraging JavaScriptCore's internal APIs through the InternalESModuleForSwiftJavaScriptCore framework. The implementation adds both synchronous and asynchronous ESModule evaluation capabilities to replace or supplement the existing require mechanism.

Key Changes:

  • Added MultiRootFSModuleLoader class implementing ESModuleLoaderDelegate to handle ESModule resolution and loading across multiple filesystem roots
  • Introduced synchronous and asynchronous evalFromURL() methods to the JSEngine class for evaluating ESModules
  • Integrated ESModule support into the JSEngine initialization with automatic module loader delegation

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

File Description
JSEngine.swift Added ESModule import, integrated MultiRootFSModuleLoader, and implemented both sync and async evalFromURL methods with promise handling
MultiRootFSModuleLoader.swift New module loader implementing ESModuleLoaderDelegate with caching, multi-root resolution, and security checks for file system boundaries
JSEngineProtocol.swift Updated protocol documentation to clarify async evalFromURL evaluates files as ES modules
MockJSEngine.swift Extended mock implementation to support both synchronous and asynchronous evalFromURL overloads for testing

Note: Based on my review, I was unable to identify the specific lines changed in the diff context to provide inline comments. However, I've identified several areas that would benefit from attention:

  1. Test Coverage: The new MultiRootFSModuleLoader class lacks dedicated unit tests, though the existing integration tests in the test suite may provide some coverage through the JSEngine tests.

  2. Return Value Inconsistency: In the async evalFromURL method (JSEngine.swift, around line 138-140), the method returns the promise object itself rather than the resolved value after awaiting it, which may not align with the expected behavior.

  3. Empty Delegate Method: The willEvaluateModule method in MultiRootFSModuleLoader (line 123-125) is implemented but empty, which may indicate incomplete functionality.

  4. Documentation: While the protocol documentation was updated, additional inline documentation explaining the module resolution algorithm and security boundaries would improve maintainability.

  5. Future Work: As noted in the PR description, the current implementation only supports module paths starting with /, ./, or ../, with plans to extend support for custom schemes using C++ APIs.


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

@cmsj
Copy link
Copy Markdown
Owner

cmsj commented Jan 4, 2026

Hmm, this is both interesting and a little terrifying. As someone who isn't deep in the JS world, what would be the benefits of doing this vs require()?

@ghostflyby
Copy link
Copy Markdown
Author

ghostflyby commented Jan 4, 2026

The current require implementation is basically a eval with no module semantics, where problems like re-evaluation and scope pollution arise.

Pros

  1. Native IDE support, better coding experience
  2. Statically exporting/importing identifiers without unexpected scope pollution
  3. Evaluate the same module exactly once even if imported multiple times, suitable for configurations as singleton
  4. Top-level await
  5. The familiar default choice for JavaScript developers
  6. Potential to adopt more JavaScript ecosystems

Cons

  1. Internal (although unchanged for long) API used, which is still harder to maintain
  2. Implementing custom module resolution rules like node_modules is easier on a require like function based module system.
  3. Bundling a JS Bundler might be a simple alternative

@cmsj
Copy link
Copy Markdown
Owner

cmsj commented Jan 4, 2026

Thanks for the reply. As someone not super familiar with the JS ecosystem, it's somewhat hard for me to evaluate this, but I'll describe where my thoughts are right now:

I'm concerned about taking on subtle, complex code at this stage of the project. I don't really know what the long-term options are going to be for using/installing third party JS code, and I'm not sure if it's a good idea to aim for something like Node compatibility or not.

I think I'd like us to focus on the core functionality and architecture first, so we can match HS1 and start migrating users over.

@ghostflyby
Copy link
Copy Markdown
Author

Whatever early-stage decision on module would be technical debts once prevailing.
Even if this is not merged, is there a chance we get a CommonJS-compliant require from the very beginning stage, whose rule is relatively easy to implement in any JavaScript environment? A require evaluating directly in the global context cannot be identified by language servers.

Module syntax has nothing to do with nodejs, we could support a module system without concerning npm or node:packages.

@alkene5
Copy link
Copy Markdown
Contributor

alkene5 commented Feb 28, 2026

What if we just embed esbuild directly? 🤓👆

dmgerman pushed a commit to dmgerman/Hammerspoon2 that referenced this pull request Mar 9, 2026
…ests

Production fixes:
- Move default IPC port to closure-scoped var to survive JSExport GC (cmsj#4)
- Make AKLog synchronous on main thread so getConsole() sees entries immediately (cmsj#3)
- Add rollback to cliInstall when man page symlink fails (cmsj#2)
- Use Atomic<Int> for callDepth in HSMessagePort (#1)
- Remove redundant Bundle.main.bundlePath optional casts (cmsj#8)
- Escape control chars in REPL tab completion query (cmsj#9)
- Deduplicate date format via HammerspoonLogEntry.formattedLine (cmsj#11)
- Remove dead print replacement code in hs.ipc.js (cmsj#10)

Test fixes:
- Remove misleading hammerspoonProcess field in HS2CommandTests (cmsj#5)
- Read pipes concurrently to prevent deadlock on large output (cmsj#6)
- Replace unsafeBitCast with direct setObject in JSTestHarness (cmsj#7)
- Save/restore evalHistory in console tests for isolation (cmsj#13)
- Handle port name conflicts gracefully in default port tests (cmsj#12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dmgerman pushed a commit to dmgerman/Hammerspoon2 that referenced this pull request Mar 21, 2026
…ests

Production fixes:
- Move default IPC port to closure-scoped var to survive JSExport GC (cmsj#4)
- Make AKLog synchronous on main thread so getConsole() sees entries immediately (cmsj#3)
- Add rollback to cliInstall when man page symlink fails (cmsj#2)
- Use Atomic<Int> for callDepth in HSMessagePort (#1)
- Remove redundant Bundle.main.bundlePath optional casts (cmsj#8)
- Escape control chars in REPL tab completion query (cmsj#9)
- Deduplicate date format via HammerspoonLogEntry.formattedLine (cmsj#11)
- Remove dead print replacement code in hs.ipc.js (cmsj#10)

Test fixes:
- Remove misleading hammerspoonProcess field in HS2CommandTests (cmsj#5)
- Read pipes concurrently to prevent deadlock on large output (cmsj#6)
- Replace unsafeBitCast with direct setObject in JSTestHarness (cmsj#7)
- Save/restore evalHistory in console tests for isolation (cmsj#13)
- Handle port name conflicts gracefully in default port tests (cmsj#12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

4 participants