Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,24 @@ bar = Bar.new
bar.v4 => "b305f5c4-db9a-4504-b0c3-4e097a5ec8b9"
```

### Named imports

To import a specific named export from a package, use the `from:` syntax:

```ruby
class Bar < Nodo::Core
import :v4, from: 'uuid'

function :generate, <<~JS
() => {
return v4();
}
JS
end
```

This is equivalent to `import { v4 } from 'uuid'` in JavaScript.

### Aliasing requires

If the library name cannot be used as name of the constant, the `const` name
Expand Down
13 changes: 10 additions & 3 deletions lib/nodo/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ def finalize_context(context_id)

{ require: :cjs, import: :esm }.each do |method, type|
define_method method do |*mods|
deps = mods.last.is_a?(Hash) ? mods.pop : {}
mods = mods.map { |m| [m, m] }.to_h
self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package, type: type) }
opts = mods.last.is_a?(Hash) ? mods.pop : {}

if opts[:from] && mods.size == 1
name = mods.first
package = opts[:from]
self.dependencies = dependencies + [Dependency.new(name, package, type: type, named_export: name.to_s)]
else
mods = mods.map { |m| [m, m] }.to_h
self.dependencies = dependencies + mods.merge(opts).map { |name, package| Dependency.new(name, package, type: type) }
end
end
private method
end
Expand Down
9 changes: 5 additions & 4 deletions lib/nodo/dependency.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module Nodo
class Dependency
attr_reader :name, :package, :type
attr_reader :name, :package, :type, :named_export

def initialize(name, package, type:)
@name, @package, @type = name, package, type
def initialize(name, package, type:, named_export: nil)
@name, @package, @type, @named_export = name, package, type, named_export
end

def to_js
Expand All @@ -30,10 +30,11 @@ def to_cjs
end

def to_esm
extract = named_export ? "[#{named_export.to_json}]" : ""
<<~JS
const #{name} = __nodo_klass__.#{name} = await (async () => {
try {
return await nodo.import(#{package.to_json});
return (await nodo.import(#{package.to_json}))#{extract};
} catch(e) {
e.nodo_dependency = #{package.to_json};
throw e;
Expand Down
93 changes: 65 additions & 28 deletions test/nodo_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def test_function
end
assert_equal 'Hello Nodo!', nodo.new.say_hi('Nodo')
end

def test_require
nodo = Class.new(Nodo::Core) do
require :fs
Expand All @@ -17,7 +17,7 @@ def test_require
assert_equal true, nodo.instance.exists_file(__FILE__)
assert_equal false, nodo.instance.exists_file('FOOBARFOO')
end

def test_require_npm
nodo = Class.new(Nodo::Core) do
require :uuid
Expand All @@ -26,23 +26,23 @@ def test_require_npm
assert uuid = nodo.new.v4
assert_equal 36, uuid.size
end

def test_const
nodo = Class.new(Nodo::Core) do
const :FOOBAR, 123
function :get_const, "() => FOOBAR"
end
assert_equal 123, nodo.new.get_const
end

def test_script
nodo = Class.new(Nodo::Core) do
script "var somevar = 99;"
function :get_somevar, "() => somevar"
end
assert_equal 99, nodo.new.get_somevar
end

def test_deferred_script_definition_by_block
nodo = Class.new(Nodo::Core) do
singleton_class.attr_accessor :value
Expand All @@ -54,7 +54,7 @@ def test_deferred_script_definition_by_block
nodo.value = 123
assert_equal 123, nodo.new.get_somevar
end

def test_cannot_define_both_code_and_block_for_script
assert_raises ArgumentError do
Class.new(Nodo::Core) do
Expand All @@ -64,14 +64,14 @@ def test_cannot_define_both_code_and_block_for_script
end
end
end

def test_async_await
nodo = Class.new(Nodo::Core) do
function :do_something, "async () => { return await 'resolved'; }"
end
assert_equal 'resolved', nodo.new.do_something
end

def test_inheritance
klass = Class.new(Nodo::Core) do
function :foo, "() => 'superclass'"
Expand All @@ -86,15 +86,15 @@ def test_inheritance
assert_equal 'callingsuperclass', subclass.new.bar
assert_equal 'callingsubsubclass', subsubclass.new.bar
end

def test_class_function
nodo = Class.new(Nodo::Core) do
function :hello, "() => 'world'"
class_function :hello
end
assert_equal 'world', nodo.hello
end

def test_syntax
nodo = Class.new(Nodo::Core) do
function :test, timeout: nil, code: <<~'JS'
Expand All @@ -103,7 +103,7 @@ def test_syntax
end
assert_equal [1, 2, 3], nodo.new.test
end

def test_deferred_function_definition_by_block
nodo = lambda do
Class.new(Nodo::Core) do
Expand All @@ -119,7 +119,7 @@ def test_deferred_function_definition_by_block
assert_equal [1, nil, 3], nodo.().new.test
assert_equal [1, 222, 3], nodo.().tap { |klass| klass.value = 222 }.new.test
end

def test_cannot_define_both_code_and_block_for_function
assert_raises ArgumentError do
Class.new(Nodo::Core) do
Expand All @@ -129,7 +129,7 @@ def test_cannot_define_both_code_and_block_for_function
end
end
end

def test_code_is_required
assert_raises ArgumentError do
Class.new(Nodo::Core) do
Expand All @@ -138,7 +138,7 @@ def test_code_is_required
end
end
end

def test_timeout
nodo = Class.new(Nodo::Core) do
function :sleep, timeout: 1, code: <<~'JS'
Expand All @@ -149,15 +149,15 @@ def test_timeout
nodo.new.sleep(2)
end
end

def test_internal_method_names_are_reserved
assert_raises ArgumentError do
Class.new(Nodo::Core) do
function :tmpdir, code: "() => '.'"
end
end
end

def test_logging
with_logger test_logger do
assert_raises(Nodo::JavaScriptError) do
Expand All @@ -178,25 +178,25 @@ def test_require_dependency_error
end
end
end

def test_evaluation
assert_equal 8, Class.new(Nodo::Core).new.evaluate('3 + 5')
end

def test_evaluation_can_access_constants
nodo = Class.new(Nodo::Core) do
const :FOO, 'bar'
end
assert_equal 'barfoo', nodo.new.evaluate('FOO + "foo"')
end

def test_evaluation_can_access_functions
nodo = Class.new(Nodo::Core) do
function :hello, code: "(name) => `Hello ${name}!`"
end
assert_equal 'Hello World!', nodo.new.evaluate('hello("World")')
end

def test_evaluation_contexts_properties_are_shared_between_instances
nodo = Class.new(Nodo::Core) do
const :LIST, []
Expand All @@ -209,7 +209,7 @@ def test_evaluation_contexts_properties_are_shared_between_instances
assert_equal %w[one two], one.evaluate('list()')
assert_equal %w[one two], two.evaluate('list()')
end

def test_evaluation_contexts_locals_are_separated_by_instance
nodo = Class.new(Nodo::Core)
one = nodo.new
Expand All @@ -219,26 +219,26 @@ def test_evaluation_contexts_locals_are_separated_by_instance
assert_equal %w[one], one.evaluate('list')
assert_equal %w[two], two.evaluate('list')
end

def test_evaluation_can_require_on_its_own
nodo = Class.new(Nodo::Core).new
nodo.evaluate('const uuid = require("uuid")')
uuid = nodo.evaluate('uuid.v4()')
assert_uuid uuid
end

def test_evaluation_can_access_requires
nodo = Class.new(Nodo::Core) { require :uuid }
uuid = nodo.new.evaluate('uuid.v4()')
assert_uuid uuid
end

def test_cannot_instantiate_core
assert_raises Nodo::ClassError do
Nodo::Core.new
end
end

def test_dynamic_imports_in_functions
klass = Class.new(Nodo::Core) do
function :v4, <<~JS
Expand All @@ -254,7 +254,7 @@ def self.name; "UUIDGen"; end
assert_uuid uuid_2 = nodo.v4
assert uuid_1 != uuid_2
end

def test_dynamic_imports_in_evaluation
nodo = Class.new(Nodo::Core)
uuid = nodo.new.evaluate("nodo.import('uuid').then((uuid) => uuid.v4()).catch((e) => null)")
Expand Down Expand Up @@ -296,6 +296,43 @@ def test_evaluation_can_access_imports
assert_uuid uuid
end

def test_named_import
nodo = Class.new(Nodo::Core) do
import :v4, from: 'uuid'
function :generate, "() => v4()"
end

assert_uuid nodo.new.generate
end

def test_named_import_with_alias
nodo = Class.new(Nodo::Core) do
import :existsSync, from: 'fs'
function :exists_file, "(file) => existsSync(file)"
end

assert_equal true, nodo.instance.exists_file(__FILE__)
assert_equal false, nodo.instance.exists_file('FOOBARFOO')
end

def test_named_import_in_evaluation
nodo = Class.new(Nodo::Core) { import :v4, from: 'uuid' }
uuid = nodo.new.evaluate('v4()')
assert_uuid uuid
end

def test_named_import_dependency_error
with_logger nil do
nodo = Class.new(Nodo::Core) do
import :foo, from: 'nonexistent-package-12345'
end

assert_raises Nodo::DependencyError do
nodo.new
end
end
end

private

def test_logger
Expand All @@ -305,15 +342,15 @@ def error(msg); errors << msg; end
self
end
end

def with_logger(logger)
prev_logger = Nodo.logger
Nodo.logger = logger
yield
ensure
Nodo.logger = prev_logger
end

def assert_uuid(obj)
assert_match /\A\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\z/, obj
end
Expand Down