diff --git a/README.md b/README.md index d141757..2f901f8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ An experimental tool to generate JavaScript code from Crystal code. * [Code Snippets](#javascript-code) * [`_call`](#_call) * [`async` / `await`](#async--await) + * [Strict Mode (opt-in)](#strict-mode-opt-in) + * [Browser API Wrappers](#browser-api-wrappers) * [Functions](#javascript-functions) * [Classes](#javascript-classes) * [Files](#javascript-files) @@ -88,6 +90,62 @@ end puts MyAsyncSnippet.to_js ``` +#### Strict Mode (opt-in) + +You can enable strict mode per generated unit: + +- `JS::Code`: `def_to_js strict: true do ... end` +- `JS::File`: `def_to_js strict: true do ... end` +- `JS::Module`: `def_to_js strict: true do ... end` + +In strict mode: + +- Referencing/calling undeclared JS identifiers raises a compile-time error. +- `_literal_js(...)` is rejected at compile-time. + +```crystal +require "js" + +class MyStrictCode < JS::Code + def_to_js strict: true do + console.log("ready") + end +end +``` + +### Browser API Wrappers + +In strict mode, method calls without an explicit receiver are resolved against a default browser context object. +For now, this context exposes `console` with: + +- `log` +- `info` +- `warn` +- `error` + +Use regular-looking calls (instead of wrapper constants or `_literal_js`): + +```crystal +class MyConsoleCode < JS::Code + def_to_js strict: true do + console.log("Hello", 7, true) + end +end +``` + +#### Adding wrappers iteratively + +Use this pattern for additional browser APIs: + +1. Add/update `JS::Context::Browser` with the new receiverless entrypoint (like `console`). +2. Add a wrapper under `src/js/context/.cr` in `JS::Context`. +3. Inherit browser wrappers from `JS::Context::ContextObject`, which stores the current JS call chain and provides call-chain initialization. +4. Return a typed browser context object from each wrapper method (for now, `console.log/info/warn/error` return `JS::Context::Undefined`). +5. Add specs that: + - verify JS output from wrapper calls; + - verify typed return wrappers and their `to_js_ref` output; + - verify strict mode acceptance when wrappers are used. + ### JavaScript Functions If you were wondering how to define a function within `JS::Code.def_to_js` - that's not possible. Well, technically it is via a `_literal_js` call but that's dirty and there is a better way: diff --git a/spec/js/code/strict_mode_spec.cr b/spec/js/code/strict_mode_spec.cr new file mode 100644 index 0000000..1eafdb8 --- /dev/null +++ b/spec/js/code/strict_mode_spec.cr @@ -0,0 +1,75 @@ +require "../../spec_helper" + +module JS::Code::StrictModeSpec + class StrictCode < JS::Code + js_alias "doc", "document" + + def_to_js strict: true do + doc.querySelector("body") + console.info("ready") + end + end + + class LooseCode < JS::Code + def_to_js do + undeclaredThing.callMe._call + end + end + + describe "strict mode" do + it "allows declared externs and typed wrappers" do + expected = <<-JS.squish + document.querySelector("body"); + console.info("ready"); + JS + + StrictCode.to_js.should eq(expected) + end + + it "keeps loose mode backwards-compatible" do + expected = <<-JS.squish + undeclaredThing.callMe(); + JS + + LooseCode.to_js.should eq(expected) + end + + it "fails on undeclared identifiers in strict mode" do + source = <<-CR + require "./src/js" + + class StrictFailureCode < JS::Code + def_to_js strict: true do + missing_api._call + end + end + + puts StrictFailureCode.to_js + CR + + exit_code, _stdout, stderr = crystal_eval(source) + exit_code.should_not eq(0) + stderr.should contain("undefined method") + stderr.should contain("missing_api") + end + + it "fails on _literal_js in strict mode" do + source = <<-CR + require "./src/js" + + class StrictLiteralCode < JS::Code + def_to_js strict: true do + _literal_js("alert('nope')") + end + end + + puts StrictLiteralCode.to_js + CR + + exit_code, _stdout, stderr = crystal_eval(source) + exit_code.should_not eq(0) + stderr.should contain("undefined method") + stderr.should contain("_literal_js") + end + end +end diff --git a/spec/js/context/console_spec.cr b/spec/js/context/console_spec.cr new file mode 100644 index 0000000..c9e65bc --- /dev/null +++ b/spec/js/context/console_spec.cr @@ -0,0 +1,34 @@ +require "../../spec_helper" + +module JS::Context::APISpec + class StrictConsoleCode < JS::Code + def_to_js strict: true do + console.log("Hello", 7, true, nil) + console.info("Info") + console.warn("Warn") + console.error("Error") + end + end + + describe "strict browser context console calls" do + it "transpiles literal console calls in strict mode" do + expected = <<-JS.squish + console.log("Hello", 7, true, undefined); + console.info("Info"); + console.warn("Warn"); + console.error("Error"); + JS + + StrictConsoleCode.to_js.should eq(expected) + end + end + + describe "typed console return value" do + it "returns an Undefined context wrapper exposing to_js_ref" do + result = JS::Context.default.console.log("Hello") + + result.should be_a(JS::Context::Undefined) + result.to_js_ref.should eq("console.log(\"Hello\")") + end + end +end diff --git a/spec/js/file/strict_mode_spec.cr b/spec/js/file/strict_mode_spec.cr new file mode 100644 index 0000000..bc4367f --- /dev/null +++ b/spec/js/file/strict_mode_spec.cr @@ -0,0 +1,21 @@ +require "../../spec_helper" + +module JS::File::StrictModeSpec + class StrictFile < JS::File + js_alias "doc", "document" + + def_to_js strict: true do + doc.body.classList.add("active") + end + end + + describe "StrictFile.to_js" do + it "supports strict mode opt-in" do + expected = <<-JS.squish + document.body.classList.add("active"); + JS + + StrictFile.to_js.should eq(expected) + end + end +end diff --git a/spec/js/module/strict_mode_spec.cr b/spec/js/module/strict_mode_spec.cr new file mode 100644 index 0000000..aa9d4b4 --- /dev/null +++ b/spec/js/module/strict_mode_spec.cr @@ -0,0 +1,26 @@ +require "../../spec_helper" + +module JS::Module::StrictModeSpec + class StrictModule < JS::Module + js_import Controller, from: "/assets/stimulus.js" + js_alias "doc", "document" + + def_to_js strict: true do + title = doc.querySelector("title") + console.log(title) + end + end + + describe "StrictModule.to_js" do + it "supports strict mode opt-in" do + expected = <<-JS.squish + import { Controller } from "/assets/stimulus.js"; + var title; + title = document.querySelector("title"); + console.log(title); + JS + + StrictModule.to_js.should eq(expected) + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 1e85a61..4b17f24 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -6,3 +6,18 @@ class String split(/\n\s*/).join end end + +def crystal_eval(source : String) + stdout = IO::Memory.new + stderr = IO::Memory.new + status = Process.run( + "crystal", + ["eval", source], + chdir: File.expand_path("..", __DIR__), + env: {"CRYSTAL_CACHE_DIR" => "/tmp/.crystal-cache-js"}, + output: stdout, + error: stderr + ) + + {status.exit_code, stdout.to_s, stderr.to_s} +end diff --git a/src/ext/bool.cr b/src/ext/bool.cr index 2fe70bb..c9d1a54 100644 --- a/src/ext/bool.cr +++ b/src/ext/bool.cr @@ -2,4 +2,8 @@ struct Bool def self.to_js_ref "Boolean" end + + def to_js_ref + self ? "true" : "false" + end end diff --git a/src/ext/float.cr b/src/ext/float.cr new file mode 100644 index 0000000..6d5fbab --- /dev/null +++ b/src/ext/float.cr @@ -0,0 +1,11 @@ +struct Float32 + def to_js_ref + self + end +end + +struct Float64 + def to_js_ref + self + end +end diff --git a/src/ext/nil.cr b/src/ext/nil.cr new file mode 100644 index 0000000..537cf7e --- /dev/null +++ b/src/ext/nil.cr @@ -0,0 +1,5 @@ +struct Nil + def to_js_ref + "undefined" + end +end diff --git a/src/js.cr b/src/js.cr index 8921990..755a4f7 100644 --- a/src/js.cr +++ b/src/js.cr @@ -1,2 +1,3 @@ require "./js/module" +require "./js/context/*" require "./ext/*" diff --git a/src/js/code.cr b/src/js/code.cr index 9f24f15..1334e13 100644 --- a/src/js/code.cr +++ b/src/js/code.cr @@ -8,9 +8,13 @@ module JS {% JS_ALIASES[name.id.stringify] = aliased_name.id.stringify %} end - macro def_to_js(&blk) + macro def_to_js(strict = false, &blk) def self.to_js(io : IO) - JS::Code._eval_js_block(io, {{@type.resolve}}, {inline: false, nested_scope: true}) {{blk}} + JS::Code._eval_js_block( + io, + {{@type.resolve}}, + {inline: false, nested_scope: true, strict: {{strict}}} + ) {{blk}} end def self.to_js @@ -32,7 +36,7 @@ module JS {% end %} {% for exp in exps %} - {% if exp.is_a?(Call) && exp.name.stringify == "_literal_js" %} + {% if exp.is_a?(Call) && exp.name.stringify == "_literal_js" && !opts[:strict] %} {{io}} << {{exp.args.first}} {% elsif exp.is_a?(Call) && exp.name.stringify == "to_js_call" %} {{io}} << {{exp}} @@ -44,7 +48,7 @@ module JS {% elsif exp.is_a?(Call) && !exp.receiver && exp.name.stringify == "await" %} {{io}} << "await " {% if exp.args.size > 0 %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.first}} end {% end %} @@ -55,19 +59,19 @@ module JS {{io}} << "async function(" {{io}} << {{exp.block.args.splat.stringify}} {{io}} << ") {" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: true}) {{exp.block}} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: true, strict: {{opts[:strict]}}}) {{exp.block}} {{io}} << "}" {% if !opts[:inline] %} {{io}} << ";" {% end %} {% elsif exp.is_a?(Call) && exp.name.stringify == "new" %} {{io}} << "new " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.receiver}} end {{io}} << "(" {% for arg, index in exp.args %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{arg}} end {% if index < exp.args.size - 1 %} @@ -78,7 +82,7 @@ module JS {% elsif exp.is_a?(Call) && exp.name.stringify == "[]" %} {{io}} << {{exp.receiver.stringify}} {{io}} << "[" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.first}} end {{io}} << "]" @@ -88,11 +92,11 @@ module JS {% elsif exp.is_a?(Call) && exp.name.stringify == "[]=" %} {{io}} << {{exp.receiver.stringify}} {{io}} << "[" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.first}} end {{io}} << "] = " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.last}} end {% if !opts[:inline] %} @@ -103,7 +107,7 @@ module JS {{io}} << "." {{io}} << {{exp.name.stringify[0..-2]}} {{io}} << " = " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.first}} end {% if !opts[:inline] %} @@ -111,30 +115,34 @@ module JS {% end %} {% elsif exp.is_a?(Call) %} {% if exp.receiver && exp.args.size == 1 && OPERATOR_CALL_NAMES.includes?(exp.name.stringify) %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.receiver}} end {{io}} << " " {{io}} << {{exp.name.stringify}} {{io}} << " " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.args.first}} end {% else %} + {% if opts[:strict] && !exp.receiver && !JS_ALIASES.has_key?(exp.name.stringify) %} + JS::Context.default.{{exp.name}} + {% end %} + {% emitted_from_strict_context = false %} {% if exp.receiver %} # TODO: Replace this whole `if` by a recursive call to this macro? {% if exp.receiver.is_a?(Call) %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.receiver}} end {% elsif exp.receiver.is_a?(Expressions) %} {% for rec_exp in exp.receiver.expressions %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{rec_exp}} end {% end %} {% elsif (exp.receiver.is_a?(Path) || exp.receiver.is_a?(TypeNode)) && exp.receiver.resolve? %} - {% if exp.receiver.resolve.has_method?(:to_js_ref) %} + {% if exp.receiver.resolve.is_a?(TypeNode) && exp.receiver.resolve.class.has_method?(:to_js_ref) %} {{io}} << {{exp.receiver}}.to_js_ref {% else %} {{io}} << {{exp.receiver.stringify}} @@ -145,8 +153,11 @@ module JS {% if exp.name.stringify != "_call" %} {{io}} << "." {% end %} + {% elsif opts[:strict] && exp.args.empty? && exp.named_args.is_a?(Nop) && !exp.block && !JS_ALIASES.has_key?(exp.name.stringify) %} + {{io}} << JS::Context.default.{{exp.name}}.to_js_ref + {% emitted_from_strict_context = true %} {% end %} - {% if exp.name.stringify != "_call" %} + {% if exp.name.stringify != "_call" && !emitted_from_strict_context %} {{io}} << {{JS_ALIASES[exp.name.stringify] || exp.name.stringify}} {% end %} {% has_named_args = !exp.named_args.is_a?(Nop) %} @@ -154,7 +165,7 @@ module JS {{io}} << "(" {% end %} {% for arg, index in exp.args %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{arg}} end {% if index < exp.args.size - 1 || exp.block || has_named_args %} @@ -166,7 +177,7 @@ module JS {% for named_arg, index in exp.named_args %} {{io}} << {{named_arg.name.stringify}} {{io}} << ": " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{named_arg.value}} end {% if index < exp.named_args.size - 1 %} @@ -182,7 +193,7 @@ module JS {{io}} << "function(" {{io}} << {{exp.block.args.splat.stringify}} {{io}} << ") {" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: true}) {{exp.block}} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: true, strict: {{opts[:strict]}}}) {{exp.block}} {{io}} << "}" {% end %} {% if exp.args.size > 0 || exp.block || exp.name.stringify == "_call" || has_named_args %} @@ -195,11 +206,11 @@ module JS {% elsif exp.is_a?(Path) %} {% parent_namespace = namespace.stringify.split("::")[0..-2].join("::").id %} {% relative_path = exp.global? ? exp.stringify.gsub(/\A::/, "") : exp %} - {% if (type = exp.resolve?) && type.class.has_method?("to_js_ref") %} + {% if (type = exp.resolve?) && type.is_a?(TypeNode) && type.class.has_method?("to_js_ref") %} {{io}} << {{exp}}.to_js_ref - {% elsif (type = parse_type("#{namespace}::#{relative_path.id}").resolve?) && type.class.has_method?("to_js_ref") %} + {% elsif (type = parse_type("#{namespace}::#{relative_path.id}").resolve?) && type.is_a?(TypeNode) && type.class.has_method?("to_js_ref") %} {{io}} << {{type}}.to_js_ref - {% elsif (type = parse_type("#{parent_namespace}::#{relative_path.id}").resolve?) && type.class.has_method?("to_js_ref") %} + {% elsif (type = parse_type("#{parent_namespace}::#{relative_path.id}").resolve?) && type.is_a?(TypeNode) && type.class.has_method?("to_js_ref") %} {{io}} << {{type}}.to_js_ref {% else %} {{io}} << {{exp.stringify}} @@ -207,7 +218,7 @@ module JS {% elsif exp.is_a?(ArrayLiteral) %} {{io}} << "[" {% for element, index in exp %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{element}} end {% if index < exp.size - 1 %} @@ -223,7 +234,7 @@ module JS {% for key, i in exp.keys %} {{io}} << {{key.id.stringify}} {{io}} << ": " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp[key]}} end {% if i < exp.size - 1 %} @@ -236,17 +247,17 @@ module JS {% end %} {% elsif exp.is_a?(If) %} {{io}} << "if (" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.cond}} end {{io}} << ") {" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.then}} end {{io}} << "}" {% if !exp.else.is_a?(Nop) %} {{io}} << " else {" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.else}} end {{io}} << "}" @@ -255,7 +266,7 @@ module JS {{exp.target}} = nil {{io}} << {{exp.target.stringify}} {{io}} << " = " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.value}} end {% if !opts[:inline] %} @@ -263,7 +274,7 @@ module JS {% end %} {% elsif exp.is_a?(Return) %} {{io}} << "return " - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false}) do {{ blk.args.empty? ? "".id : "|#{blk.args.splat}|".id }} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: true, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.exp}} end {% if !opts[:inline] %} @@ -271,23 +282,23 @@ module JS {% end %} {% elsif exp.is_a?(ProcLiteral) %} {{io}} << "({{exp.args.map(&.name).splat}}) => {" - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{blk.args.empty? ? "".id : "|#{blk.args.splat}|".id}} {{exp.body}} end {{io}} << "}" {% elsif exp.is_a?(MacroIf) %} \{% if {{exp.cond}} %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{exp.then}} end \{% else %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{exp.else}} end \{% end %} {% elsif exp.is_a?(MacroFor) %} \{% for {{exp.vars.splat}} in {{exp.exp}} %} - JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false}) do + JS::Code._eval_js_block({{io}}, {{namespace}}, {inline: false, nested_scope: false, strict: {{opts[:strict]}}}) do {{exp.body}} end \{% end %} diff --git a/src/js/context/browser.cr b/src/js/context/browser.cr new file mode 100644 index 0000000..0be8bdb --- /dev/null +++ b/src/js/context/browser.cr @@ -0,0 +1,15 @@ +require "./console" + +module JS + module Context + class Browser + def console : JS::Context::Console + JS::Context::Console.new + end + end + + def self.default : JS::Context::Browser + JS::Context::Browser.new + end + end +end diff --git a/src/js/context/console.cr b/src/js/context/console.cr new file mode 100644 index 0000000..fc27b7b --- /dev/null +++ b/src/js/context/console.cr @@ -0,0 +1,32 @@ +require "./context_object" +require "./undefined" + +module JS + module Context + class Console < JS::Context::ContextObject + def initialize + super("console") + end + + def log(*args : JS::Context::CallArgument) : JS::Context::Undefined + build_call("log", *args) + end + + def info(*args : JS::Context::CallArgument) : JS::Context::Undefined + build_call("info", *args) + end + + def warn(*args : JS::Context::CallArgument) : JS::Context::Undefined + build_call("warn", *args) + end + + def error(*args : JS::Context::CallArgument) : JS::Context::Undefined + build_call("error", *args) + end + + private def build_call(name : String, *args : JS::Context::CallArgument) : JS::Context::Undefined + JS::Context::Undefined.new(to_js_ref, name, *args) + end + end + end +end diff --git a/src/js/context/context_object.cr b/src/js/context/context_object.cr new file mode 100644 index 0000000..1e1898a --- /dev/null +++ b/src/js/context/context_object.cr @@ -0,0 +1,39 @@ +module JS + module Context + alias CallArgument = Nil | Bool | Int::Primitive | Float32 | Float64 | String | JS::Context::ContextObject + + abstract class ContextObject + getter to_js_ref : String + + protected def initialize(@to_js_ref : String) + end + + def initialize(preceding_call_chain : String, method_name : String, *args : JS::Context::CallArgument) + @to_js_ref = JS::Context.build_call_chain(preceding_call_chain, method_name, *args) + end + end + + # Keep chain generation centralized so all browser context wrappers emit identical JS call syntax. + def self.build_call_chain(preceding_call_chain : String, method_name : String, *args : JS::Context::CallArgument) : String + String.build do |io| + unless preceding_call_chain.empty? + io << preceding_call_chain + io << "." + end + io << method_name + if !preceding_call_chain.empty? || !args.empty? + io << "(" + serialize_args(io, *args) + io << ")" + end + end + end + + def self.serialize_args(io : IO, *args : JS::Context::CallArgument) : Nil + args.each_with_index do |arg, index| + io << ", " unless index.zero? + io << arg.to_js_ref + end + end + end +end diff --git a/src/js/context/undefined.cr b/src/js/context/undefined.cr new file mode 100644 index 0000000..2b2c61a --- /dev/null +++ b/src/js/context/undefined.cr @@ -0,0 +1,8 @@ +require "./context_object" + +module JS + module Context + class Undefined < JS::Context::ContextObject + end + end +end diff --git a/src/js/file.cr b/src/js/file.cr index 6fc9920..ac0fdd5 100644 --- a/src/js/file.cr +++ b/src/js/file.cr @@ -45,11 +45,11 @@ module JS end end - macro def_to_js(&blk) - def_to_js({{@type}}) {{blk}} + macro def_to_js(strict = false, &blk) + def_to_js({{@type}}, strict: {{strict}}) {{blk}} end - macro def_to_js(namespace, &blk) + macro def_to_js(namespace, strict = false, &blk) def self.to_js(io : IO) @@js_classes.each do |js_class| js_class.to_js(io) @@ -57,7 +57,11 @@ module JS @@js_functions.each do |func| func.to_js(io) end - JS::Code._eval_js_block(io, {{namespace}}, {inline: false, nested_scope: true}) {{blk}} + JS::Code._eval_js_block( + io, + {{namespace}}, + {inline: false, nested_scope: true, strict: {{strict}}} + ) {{blk}} end def self.to_js diff --git a/src/js/function.cr b/src/js/function.cr index 199785d..35a6811 100644 --- a/src/js/function.cr +++ b/src/js/function.cr @@ -37,7 +37,11 @@ module JS {% else %} io << "function #{function_name}({{blk.args.splat}}) {" {% end %} - JS::Code._eval_js_block(io, {{@type.resolve}}, {inline: false, nested_scope: true}) {{blk}} + JS::Code._eval_js_block( + io, + {{@type.resolve}}, + {inline: false, nested_scope: true, strict: false} + ) {{blk}} io << "}" end diff --git a/src/js/method.cr b/src/js/method.cr index 58e8e55..acf5440 100644 --- a/src/js/method.cr +++ b/src/js/method.cr @@ -14,7 +14,11 @@ module JS def self.to_js(io : IO) io << "#{function_name}({{blk.args.splat}}) {" - JS::Code._eval_js_block(io, {{@type.resolve}}, {inline: false, nested_scope: true}) {{blk}} + JS::Code._eval_js_block( + io, + {{@type.resolve}}, + {inline: false, nested_scope: true, strict: false} + ) {{blk}} io << "}" end diff --git a/src/js/module.cr b/src/js/module.cr index 5687aa7..0f32aa4 100644 --- a/src/js/module.cr +++ b/src/js/module.cr @@ -8,8 +8,8 @@ module JS @@js_imports << ("import { {{names.map(&.id).splat}} } from \"" + {{from}} + "\";") end - macro def_to_js(&blk) - JS::File.def_to_js({{@type}}) {{blk}} + macro def_to_js(strict = false, &blk) + JS::File.def_to_js({{@type}}, strict: {{strict}}) {{blk}} def self.to_js(io : IO) @@js_imports.join(io, "\n")