-
Notifications
You must be signed in to change notification settings - Fork 25
feat: support nodeModulesResolver
#1516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import { test, expect } from "bun:test" | ||
| import { CircuitRunner } from "lib/runner/CircuitRunner" | ||
|
|
||
| test("nodeModulesResolver: should resolve modules not in fsMap", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| // Create a custom resolver that returns a mock module | ||
| const fakeResolver = async (modulePath: string) => { | ||
| if (modulePath === "test-package") { | ||
| return ` | ||
| export const testValue = "resolved from custom resolver" | ||
| export default { message: "hello from resolver" } | ||
| ` | ||
| } | ||
| throw new Error("Package not found") | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| await runner.execute(` | ||
| import { testValue } from "test-package" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| // If we get here without errors, the resolver worked | ||
| expect( | ||
| runner._executionContext?.preSuppliedImports["test-package"], | ||
| ).toBeDefined() | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should handle scoped packages", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| const fakeResolver = async (modulePath: string) => { | ||
| if (modulePath === "@test/scoped-package") { | ||
| return `export const scopedValue = "from scoped package"` | ||
| } | ||
| throw new Error("Package not found") | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| await runner.execute(` | ||
| import { scopedValue } from "@test/scoped-package" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| expect( | ||
| runner._executionContext?.preSuppliedImports["@test/scoped-package"], | ||
| ).toBeDefined() | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should handle subpath imports", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| const fakeResolver = async (modulePath: string) => { | ||
| if (modulePath === "@test/package/submodule") { | ||
| return `export const subValue = "from submodule"` | ||
| } | ||
| throw new Error("Package not found") | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| await runner.execute(` | ||
| import { subValue } from "@test/package/submodule" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| expect( | ||
| runner._executionContext?.preSuppliedImports["@test/package/submodule"], | ||
| ).toBeDefined() | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should handle nested subpaths", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| const fakeResolver = async (modulePath: string) => { | ||
| if (modulePath === "regular-package/dist/index") { | ||
| return `export const nestedValue = "from nested path"` | ||
| } | ||
| throw new Error("Package not found") | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| await runner.execute(` | ||
| import { nestedValue } from "regular-package/dist/index" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| expect( | ||
| runner._executionContext?.preSuppliedImports["regular-package/dist/index"], | ||
| ).toBeDefined() | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should fallback to npm CDN when resolver throws", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| const fakeResolver = async () => { | ||
| throw new Error("Package not found") // Always throw | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| // Since the resolver throws, it should fall back to npm CDN | ||
| // Missing packages will fail with jsdelivr error | ||
| await expect(async () => { | ||
| await runner.execute(` | ||
| import { missing } from "missing-package" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
| }).toThrow() // Will throw jsdelivr error | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should prefer fsMap over resolver", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| let resolverCalled = false | ||
| const fakeResolver = async () => { | ||
| resolverCalled = true | ||
| return `export const value = "from resolver"` | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: fakeResolver, | ||
| } | ||
|
|
||
| await runner.executeWithFsMap({ | ||
| entrypoint: "index.tsx", | ||
| fsMap: { | ||
| "index.tsx": ` | ||
| import { value } from "my-package" | ||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `, | ||
| "node_modules/my-package/index.ts": `export const value = "from fsMap"`, | ||
| "node_modules/my-package/package.json": JSON.stringify({ | ||
| main: "index.ts", | ||
| }), | ||
| }, | ||
| }) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| // Resolver should NOT have been called since the package exists in fsMap | ||
| expect(resolverCalled).toBe(false) | ||
| expect( | ||
| runner._executionContext?.preSuppliedImports["my-package"], | ||
| ).toBeDefined() | ||
| }) | ||
|
|
||
| test("nodeModulesResolver: should work with CDN-style resolver", async () => { | ||
| const runner = new CircuitRunner() | ||
|
|
||
| // Simulate a CDN resolver | ||
| const cdnResolver = async (modulePath: string) => { | ||
| // Simulate fetching from a CDN | ||
| if (modulePath === "lodash") { | ||
| return ` | ||
| export function get(obj, path) { | ||
| return "mocked lodash get" | ||
| } | ||
| ` | ||
| } | ||
| throw new Error("Package not found") | ||
| } | ||
|
|
||
| runner._circuitRunnerConfiguration.platform = { | ||
| nodeModulesResolver: cdnResolver, | ||
| } | ||
|
|
||
| await runner.execute(` | ||
| import { get } from "lodash" | ||
|
|
||
| circuit.add(<board width="10mm" height="10mm" />) | ||
| `) | ||
|
|
||
| await runner.renderUntilSettled() | ||
|
|
||
| expect(runner._executionContext?.preSuppliedImports.lodash).toBeDefined() | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the custom
nodeModulesResolverreturns code for a bare specifier, the module is injected into the virtual FS asnode_modules/${importName}.ts(lines 33‑46). That flattens the whole package into a single file at the root ofnode_modules.importLocalFileresolves relative imports based ondirname(fsPath), so any./fooinside the fetched module now resolves tonode_modules/fooinstead ofnode_modules/<package>/foo. As a result, any real package source that contains relative imports (the common case for CDN bundles) will immediately fail to resolve its own dependencies when loaded via the resolver. The synthetic file needs to live inside a package directory (e.g.node_modules/<spec>/index.ts) so that relative paths match Node semantics.Useful? React with 👍 / 👎.