Skip to content

Refactor playground module system to support multi-file examples#84

Merged
AgentEnder merged 2 commits intomainfrom
claude/fix-sandbox-imports-YLZgz
Apr 18, 2026
Merged

Refactor playground module system to support multi-file examples#84
AgentEnder merged 2 commits intomainfrom
claude/fix-sandbox-imports-YLZgz

Conversation

@AgentEnder
Copy link
Copy Markdown
Owner

Summary

Refactored the playground code execution engine to support multi-file examples with proper CommonJS module resolution, replacing the previous single-file approach that used regex stripping and a preamble-based import injection system.

Key Changes

  • Module graph support: Introduced a file map (codeFileMap) that tracks all code and JSON files, enabling proper module resolution across multiple files in the playground.

  • Pre-transpilation and caching: Implemented transpileRec() to walk the require graph up-front and transpile all reachable modules to CommonJS before execution, allowing synchronous require() calls during runtime.

  • Proper module system: Built a complete CommonJS module loader with:

    • ModuleRecord tracking for each module's exports and load state
    • loadSubModule() for lazy-loading sub-modules with circular dependency support
    • createRequire() factory that returns a require function scoped to each module's directory
  • Path resolution: Added resolveRelative() utility that implements Node.js-style module resolution, including:

    • Relative path normalization (dirname(), normalizePath())
    • Extension resolution (.ts, .tsx, .js, .jsx, .mjs, .cjs, .json)
    • Index file fallback (index.ts, index.js, etc.)
  • Simplified transpilation: Replaced complex regex-based import stripping with straightforward TypeScript transpilation to CommonJS, handling all TS syntax naturally.

  • Host module wrapping: Wrapped cli-forge and @cli-forge/parser modules to ensure their default exports work correctly with TypeScript's __importDefault helper.

  • Entry point rewriting: Moved the .forge().forge(__argv__) rewrite into the transpilation phase for the entry file only.

Implementation Details

  • Each module executes in its own Function scope with proper module, exports, require, __filename, and __dirname bindings, matching Node.js CJS semantics.
  • The entry point runs in an AsyncFunction to support top-level await if needed.
  • Circular dependencies are handled by returning partially-loaded module exports during recursive loads.
  • JSON files are transpiled to module.exports = {...} statements.

https://claude.ai/code/session_01B3gDdZJb71jjou2L55vqfk

The playground previously loaded only the entry file and regex-stripped
every non-cli-forge import, so multi-file examples like composable-builders
and multi-command-cli silently broke whenever they did
`import { x } from './commands/build'` — the sub-module identifier became
undefined at runtime.

Replace the single-file ESNext transpile + binding-preamble dance with a
minimal CJS loader: pre-transpile every file reachable from the entry to
CommonJS, walk the require() graph, then execute with a custom require()
that resolves relative specifiers against the in-memory file map and
returns the host `cli-forge` / `@cli-forge/parser` modules for bare
specifiers. Sub-modules run in a plain Function for sync require
semantics; the entry still runs in an AsyncFunction so top-level await
keeps working.
@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented Apr 12, 2026

View your CI Pipeline Execution ↗ for commit 047bfe0

Command Status Duration Result
nx run-many -t build test e2e lint ✅ Succeeded 4m 58s View ↗
nx build docs-site ✅ Succeeded 1m 9s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-12 20:27:12 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 12, 2026

Docs Preview: https://craigory.dev/cli-forge/pr/84/

Deployed commit: 047bfe0 (047bfe0)

github-actions Bot added a commit that referenced this pull request Apr 12, 2026
The playground previously only had a Monaco model for the active tab, so
the TypeScript language service couldn't resolve relative imports between
sibling files — `import { buildCommand } from './commands/build'` showed
up as an unresolved specifier even when the file existed in the example.

Capture the monaco namespace during onMount and add a sync effect that
creates / updates / disposes models for every code and JSON file in the
example at `file:///${path}`. The active file stays owned by the Editor
component (so typing still works); sibling models get refreshed when a
different example is loaded. Also flip on `resolveJsonModule` so examples
that import JSON config files get types too.
github-actions Bot added a commit that referenced this pull request Apr 12, 2026
@AgentEnder AgentEnder merged commit c3b0bb0 into main Apr 18, 2026
4 checks passed
@AgentEnder AgentEnder deleted the claude/fix-sandbox-imports-YLZgz branch April 18, 2026 00:53
github-actions Bot added a commit that referenced this pull request Apr 18, 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.

2 participants