clementine 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/README.md +52 -0
- data/Rakefile +1 -0
- data/clementine.gemspec +23 -0
- data/lib/clementine.rb +27 -0
- data/lib/clementine/clementine_rails.rb +8 -0
- data/lib/clementine/clojurescript_engine.rb +49 -0
- data/lib/clementine/clojurescript_engine_mri.rb +65 -0
- data/lib/clementine/clojurescript_template.rb +21 -0
- data/lib/clementine/options.rb +9 -0
- data/lib/clementine/version.rb +3 -0
- data/test/clojurescript_engine_test.rb +46 -0
- data/test/options_test.rb +22 -0
- data/vendor/assets/bin/cljsc.clj +21 -0
- data/vendor/assets/lib/clojure.jar +0 -0
- data/vendor/assets/lib/compiler.jar +0 -0
- data/vendor/assets/lib/goog.jar +0 -0
- data/vendor/assets/lib/js.jar +0 -0
- data/vendor/assets/src/clj/cljs/closure.clj +823 -0
- data/vendor/assets/src/clj/cljs/compiler.clj +1341 -0
- data/vendor/assets/src/clj/cljs/core.clj +702 -0
- data/vendor/assets/src/clj/cljs/repl.clj +162 -0
- data/vendor/assets/src/clj/cljs/repl/browser.clj +341 -0
- data/vendor/assets/src/clj/cljs/repl/rhino.clj +170 -0
- data/vendor/assets/src/cljs/cljs/core.cljs +3330 -0
- data/vendor/assets/src/cljs/cljs/nodejs.cljs +11 -0
- data/vendor/assets/src/cljs/cljs/nodejs_externs.js +2 -0
- data/vendor/assets/src/cljs/cljs/nodejscli.cljs +9 -0
- data/vendor/assets/src/cljs/cljs/reader.cljs +360 -0
- data/vendor/assets/src/cljs/clojure/browser/dom.cljs +106 -0
- data/vendor/assets/src/cljs/clojure/browser/event.cljs +100 -0
- data/vendor/assets/src/cljs/clojure/browser/net.cljs +182 -0
- data/vendor/assets/src/cljs/clojure/browser/repl.cljs +109 -0
- data/vendor/assets/src/cljs/clojure/set.cljs +162 -0
- data/vendor/assets/src/cljs/clojure/string.cljs +160 -0
- data/vendor/assets/src/cljs/clojure/walk.cljs +94 -0
- data/vendor/assets/src/cljs/clojure/zip.cljs +291 -0
- metadata +103 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
Clementine
|
2
|
+
====
|
3
|
+
|
4
|
+
* https://github.com/yokolet/clementine
|
5
|
+
* http://yokolet.blogspot.com/2011/11/clojurescript-on-rails-asset-pipeline.html
|
6
|
+
* http://yokolet.blogspot.com/2011/11/tilt-template-for-clojurescript.html
|
7
|
+
|
8
|
+
Description
|
9
|
+
-----------
|
10
|
+
|
11
|
+
Clementine is a gem to use ClojureScript (https://github.com/clojure/clojurescript) from Ruby.
|
12
|
+
Clementine is a Tilt (https://github.com/rtomayko/tilt) Template, which is available to use
|
13
|
+
on Rails asset pipeline. Also, it is avilable to use in a Tilt way.
|
14
|
+
|
15
|
+
Clementine runs on Rails 3.1 and later.
|
16
|
+
|
17
|
+
Clementine supports JRuby and CRuby. When you use from CRuby, make sure java command is on your PATH.
|
18
|
+
|
19
|
+
Installation
|
20
|
+
-----------
|
21
|
+
|
22
|
+
Clone https://github.com/yokolet/clementine, then
|
23
|
+
edit your Gemfile with specific path to Clemetine.
|
24
|
+
|
25
|
+
For example:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem 'clementine', :path => "/Users/yoko/Projects/clementine"
|
29
|
+
```
|
30
|
+
|
31
|
+
Configuration
|
32
|
+
-----------
|
33
|
+
|
34
|
+
Create clementine.rb file in your ${Rails.root}/config/initializer directory.
|
35
|
+
|
36
|
+
Examples:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Clementine.options[:optimizations] = :simple
|
40
|
+
Clementine.options[:output_dir] = "assets/javascripts"
|
41
|
+
```
|
42
|
+
|
43
|
+
Available options:
|
44
|
+
|
45
|
+
```
|
46
|
+
KEY VALUES
|
47
|
+
------------------ -----------------------
|
48
|
+
:optimazation :simple,:whitespace,:advanced
|
49
|
+
:target :nodejs
|
50
|
+
:output_dir directory name (:output_dir will be converted to ":output-dir")
|
51
|
+
:output_to file name (:output_to will be converted to ":output-to")
|
52
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/clementine.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "clementine/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "clementine"
|
7
|
+
s.version = Clementine::VERSION
|
8
|
+
s.authors = ["Yoko Harada"]
|
9
|
+
s.email = ["yokolet@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/yokolet/clementine"
|
11
|
+
s.summary = %q{clojurescript tilt template gem}
|
12
|
+
s.description = %q{clojurescript tilt template gem and available to use on Rails asset pipeline.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
# specify any dependencies here; for example:
|
20
|
+
# s.add_development_dependency "rspec"
|
21
|
+
# s.add_runtime_dependency "rest-client"
|
22
|
+
s.add_dependency "tilt"
|
23
|
+
end
|
data/lib/clementine.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "clementine/version"
|
3
|
+
require 'clementine/options'
|
4
|
+
|
5
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
|
6
|
+
require "java"
|
7
|
+
|
8
|
+
CLOJURESCRIPT_HOME = File.dirname(__FILE__) + "/../vendor/assets"
|
9
|
+
$: << CLOJURESCRIPT_HOME + "/lib"
|
10
|
+
require 'clojure'
|
11
|
+
|
12
|
+
%w{compiler.jar goog.jar js.jar}.each {|name| $CLASSPATH << CLOJURESCRIPT_HOME + "/lib/" + name}
|
13
|
+
%w{clj cljs}.each {|path| $CLASSPATH << CLOJURESCRIPT_HOME + "/src/" + path}
|
14
|
+
|
15
|
+
require "clementine/clojurescript_engine"
|
16
|
+
require "clementine/clojurescript_template"
|
17
|
+
require "clementine/clementine_rails" if defined?(Rails)
|
18
|
+
end
|
19
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
|
20
|
+
CLOJURESCRIPT_HOME = File.dirname(__FILE__) + "/../vendor/assets"
|
21
|
+
CLASSPATH = []
|
22
|
+
%w{clojure.jar compiler.jar goog.jar js.jar}.each {|name| CLASSPATH << CLOJURESCRIPT_HOME + "/lib/" + name}
|
23
|
+
%w{clj cljs}.each {|path| CLASSPATH << CLOJURESCRIPT_HOME + "/src/" + path}
|
24
|
+
require "clementine/clojurescript_engine_mri"
|
25
|
+
require "clementine/clojurescript_template"
|
26
|
+
require "clementine/clementine_rails" if defined?(Rails)
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
%w{RT Keyword PersistentHashMap}.each do |name|
|
2
|
+
java_import "clojure.lang.#{name}"
|
3
|
+
end
|
4
|
+
|
5
|
+
module Clementine
|
6
|
+
|
7
|
+
class ClojureScriptEngine
|
8
|
+
def initialize(file, options)
|
9
|
+
@file = file
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def compile
|
14
|
+
@options = Clementine.options if @options.empty?
|
15
|
+
cl_opts = PersistentHashMap.create(convert_options(@options))
|
16
|
+
RT.loadResourceScript("cljs/closure.clj")
|
17
|
+
builder = RT.var("cljs.closure", "build")
|
18
|
+
builder.invoke(@file, cl_opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
#private
|
22
|
+
def convert_options(options)
|
23
|
+
opts = {}
|
24
|
+
options = options.empty? ? default_opts : options
|
25
|
+
options.each do |k, v|
|
26
|
+
cl_key = Keyword.intern(Clementine.ruby2clj(k.to_s))
|
27
|
+
case
|
28
|
+
when (v.kind_of? Symbol)
|
29
|
+
cl_value = Keyword.intern(Clementine.ruby2clj(v.to_s))
|
30
|
+
else
|
31
|
+
cl_value = v
|
32
|
+
end
|
33
|
+
opts[cl_key] = cl_value
|
34
|
+
end
|
35
|
+
opts
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_opts
|
39
|
+
key = "output_dir"
|
40
|
+
value = ""
|
41
|
+
if defined?(Rails)
|
42
|
+
value = File.join(Rails.root, "app", "assets", "javascripts", "clementine")
|
43
|
+
else
|
44
|
+
value = Dir.pwd
|
45
|
+
end
|
46
|
+
{key => value}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Clementine
|
2
|
+
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class ClojureScriptEngine
|
6
|
+
def initialize(file, options)
|
7
|
+
@file = file
|
8
|
+
@options = options
|
9
|
+
@classpath = CLASSPATH
|
10
|
+
end
|
11
|
+
|
12
|
+
def compile
|
13
|
+
@options = Clementine.options if @options.empty?
|
14
|
+
begin
|
15
|
+
cmd = "#{command} #{@file} #{convert_options(@options)} 2>&1"
|
16
|
+
result = `#{cmd}`
|
17
|
+
rescue Exception
|
18
|
+
raise Error, "compression failed: #{result}"
|
19
|
+
end
|
20
|
+
unless $?.exitstatus.zero?
|
21
|
+
raise Error, result
|
22
|
+
end
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
def nailgun_prefix
|
27
|
+
server_address = Nailgun::NailgunConfig.options[:server_address]
|
28
|
+
port_no = Nailgun::NailgunConfig.options[:port_no]
|
29
|
+
"#{Nailgun::NgCommand::NGPATH} --nailgun-port #{port_no} --nailgun-server #{server_address}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_classpath_for_ng
|
33
|
+
current_cp = `#{nailgun_prefix} ng-cp`
|
34
|
+
unless current_cp.include? "clojure.jar"
|
35
|
+
puts "Initializing nailgun classpath, required clementine dependencies missing"
|
36
|
+
`#{nailgun_prefix} ng-cp #{@classpath.join " "}`
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def command
|
41
|
+
if defined? Nailgun
|
42
|
+
setup_classpath_for_ng
|
43
|
+
[nailgun_prefix, 'clojure.main', "#{CLOJURESCRIPT_HOME}/bin/cljsc.clj"].flatten.join(' ')
|
44
|
+
else
|
45
|
+
["java", '-cp', "\"#{@classpath.join ":"}\"", 'clojure.main', "#{CLOJURESCRIPT_HOME}/bin/cljsc.clj"].flatten.join(' ')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def convert_options(options)
|
51
|
+
opts = ""
|
52
|
+
options.each do |k, v|
|
53
|
+
cl_key = ":" + Clementine.ruby2clj(k.to_s)
|
54
|
+
case
|
55
|
+
when (v.kind_of? Symbol)
|
56
|
+
cl_value = ":" + Clementine.ruby2clj(v.to_s)
|
57
|
+
else
|
58
|
+
cl_value = "\"" + v + "\""
|
59
|
+
end
|
60
|
+
opts += cl_key + " " + cl_value + " "
|
61
|
+
end
|
62
|
+
opts.chop!
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'tilt/template'
|
2
|
+
|
3
|
+
module Clementine
|
4
|
+
class ClojureScriptTemplate < Tilt::Template
|
5
|
+
self.default_mime_type = 'application/javascript'
|
6
|
+
|
7
|
+
def self.engine_initialized?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_engine; end
|
12
|
+
|
13
|
+
def prepare
|
14
|
+
@engine = ClojureScriptEngine.new(@file, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def evaluate(scope, locals, &block)
|
18
|
+
@output ||= @engine.compile
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
|
3
|
+
class ClojureScriptEngineTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# Called before every test method runs. Can be used
|
6
|
+
# to set up fixture information.
|
7
|
+
def setup
|
8
|
+
require "#{File.join(File.dirname(__FILE__), "..", "lib", "clementine")}"
|
9
|
+
require "#{File.join(File.dirname(__FILE__), "..", "lib", "clementine", "clojurescript_engine")}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Called after every test method runs. Can be used to tear
|
13
|
+
# down fixture information.
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
# Do nothing
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_default_option
|
20
|
+
expect = {"output_dir" => "#{Dir.pwd}"}
|
21
|
+
engine = Clementine::ClojureScriptEngine.new("", "")
|
22
|
+
assert_equal expect, engine.default_opts
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_convert_options
|
26
|
+
options = {:optimizations => :advanced, :output_dir => "#{Dir.pwd}"}
|
27
|
+
engine = Clementine::ClojureScriptEngine.new("", "")
|
28
|
+
opts = engine.convert_options(options)
|
29
|
+
opts.each do |k, v|
|
30
|
+
assert_equal Java::clojure.lang.Keyword, k.class
|
31
|
+
assert k.to_s == ":optimizations" || k.to_s == ":output-dir"
|
32
|
+
assert v.to_s == ":advanced" || v.to_s == "#{Dir.pwd}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_created_clojure_map
|
37
|
+
options = {:optimizations => :advanced, :output_dir => "#{Dir.pwd}"}
|
38
|
+
engine = Clementine::ClojureScriptEngine.new("", "")
|
39
|
+
opts = engine.convert_options(options)
|
40
|
+
map = PersistentHashMap.create(convert_options(opts))
|
41
|
+
map.each do |k, v|
|
42
|
+
assert_equal Java::clojure.lang.Keyword, k.class
|
43
|
+
assert k.to_s == ":optimizations" || k.to_s == ":output-dir"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
|
3
|
+
class OptionsTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# Called before every test method runs. Can be used
|
6
|
+
# to set up fixture information.
|
7
|
+
def setup
|
8
|
+
require "#{File.join(File.dirname(__FILE__), "..", "lib", "clementine", "options")}"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Called after every test method runs. Can be used to tear
|
12
|
+
# down fixture information.
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
# Do nothing
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_ruby2clj
|
19
|
+
assert_equal "output-dir", Clementine.ruby2clj("output_dir")
|
20
|
+
assert_equal "output-to", Clementine.ruby2clj("output_to")
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
; Copyright (c) Rich Hickey. All rights reserved.
|
2
|
+
; The use and distribution terms for this software are covered by the
|
3
|
+
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
|
4
|
+
; which can be found in the file epl-v10.html at the root of this distribution.
|
5
|
+
; By using this software in any fashion, you are agreeing to be bound by
|
6
|
+
; the terms of this license.
|
7
|
+
; You must not remove this notice, or any other, from this software.
|
8
|
+
|
9
|
+
(require '[cljs.closure :as closure])
|
10
|
+
|
11
|
+
(defn transform-cl-args
|
12
|
+
[args]
|
13
|
+
(let [source (first args)
|
14
|
+
opts-string (apply str (interpose " " (rest args)))
|
15
|
+
options (when (> (count opts-string) 1)
|
16
|
+
(try (read-string opts-string)
|
17
|
+
(catch Exception e (println e))))]
|
18
|
+
{:source source :options (merge {:output-to :print} options)}))
|
19
|
+
|
20
|
+
(let [args (transform-cl-args *command-line-args*)]
|
21
|
+
(closure/build (:source args) (:options args)))
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,823 @@
|
|
1
|
+
; Copyright (c) Rich Hickey. All rights reserved.
|
2
|
+
; The use and distribution terms for this software are covered by the
|
3
|
+
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
|
4
|
+
; which can be found in the file epl-v10.html at the root of this distribution.
|
5
|
+
; By using this software in any fashion, you are agreeing to be bound by
|
6
|
+
; the terms of this license.
|
7
|
+
; You must not remove this notice, or any other, from this software.
|
8
|
+
|
9
|
+
(ns cljs.closure
|
10
|
+
"Compile ClojureScript to JavaScript with optimizations from Google
|
11
|
+
Closure Compiler producing runnable JavaScript.
|
12
|
+
|
13
|
+
The Closure Compiler (compiler.jar) must be on the classpath.
|
14
|
+
|
15
|
+
Use the 'build' function for end-to-end compilation.
|
16
|
+
|
17
|
+
build = compile -> add-dependencies -> optimize -> output
|
18
|
+
|
19
|
+
Two protocols are defined: IJavaScript and Compilable. The
|
20
|
+
Compilable protocol is satisfied by something which can return one
|
21
|
+
or more IJavaScripts.
|
22
|
+
|
23
|
+
With IJavaScript objects in hand, calling add-dependencies will
|
24
|
+
produce a sequence of IJavaScript objects which includes all
|
25
|
+
required dependencies from the Closure library and ClojureScript,
|
26
|
+
in dependency order. This function replaces the closurebuilder
|
27
|
+
tool.
|
28
|
+
|
29
|
+
The optimize function converts one or more IJavaScripts into a
|
30
|
+
single string of JavaScript source code using the Closure Compiler
|
31
|
+
API.
|
32
|
+
|
33
|
+
The produced output is either a single string of optimized
|
34
|
+
JavaScript or a deps file for use during development.
|
35
|
+
"
|
36
|
+
(:require [cljs.compiler :as comp]
|
37
|
+
[clojure.java.io :as io]
|
38
|
+
[clojure.string :as string])
|
39
|
+
(:import java.io.File
|
40
|
+
java.io.BufferedInputStream
|
41
|
+
java.net.URL
|
42
|
+
java.util.logging.Level
|
43
|
+
java.util.jar.JarFile
|
44
|
+
com.google.common.collect.ImmutableList
|
45
|
+
com.google.javascript.jscomp.CompilerOptions
|
46
|
+
com.google.javascript.jscomp.CompilationLevel
|
47
|
+
com.google.javascript.jscomp.ClosureCodingConvention
|
48
|
+
com.google.javascript.jscomp.JSSourceFile
|
49
|
+
com.google.javascript.jscomp.Result
|
50
|
+
com.google.javascript.jscomp.JSError
|
51
|
+
com.google.javascript.jscomp.CommandLineRunner))
|
52
|
+
|
53
|
+
(def name-chars (map char (concat (range 48 57) (range 65 90) (range 97 122))))
|
54
|
+
|
55
|
+
(defn random-char []
|
56
|
+
(nth name-chars (.nextInt (java.util.Random.) (count name-chars))))
|
57
|
+
|
58
|
+
(defn random-string [length]
|
59
|
+
(apply str (take length (repeatedly random-char))))
|
60
|
+
|
61
|
+
;; Closure API
|
62
|
+
;; ===========
|
63
|
+
|
64
|
+
(defmulti js-source-file (fn [_ source] (class source)))
|
65
|
+
|
66
|
+
(defmethod js-source-file String [^String name ^String source]
|
67
|
+
(JSSourceFile/fromCode name source))
|
68
|
+
|
69
|
+
(defmethod js-source-file File [_ ^File source]
|
70
|
+
(JSSourceFile/fromFile source))
|
71
|
+
|
72
|
+
(defmethod js-source-file BufferedInputStream [^String name ^BufferedInputStream source]
|
73
|
+
(JSSourceFile/fromInputStream name source))
|
74
|
+
|
75
|
+
(defn set-options
|
76
|
+
"TODO: Add any other options that we would like to support."
|
77
|
+
[opts ^CompilerOptions compiler-options]
|
78
|
+
(when (contains? opts :pretty-print)
|
79
|
+
(set! (.prettyPrint compiler-options) (:pretty-print opts)))
|
80
|
+
(when (contains? opts :print-input-delimiter)
|
81
|
+
(set! (.printInputDelimiter compiler-options)
|
82
|
+
(:print-input-delimiter opts))))
|
83
|
+
|
84
|
+
(defn make-options
|
85
|
+
"Create a CompilerOptions object and set options from opts map."
|
86
|
+
[opts]
|
87
|
+
(let [level (case (:optimizations opts)
|
88
|
+
:advanced CompilationLevel/ADVANCED_OPTIMIZATIONS
|
89
|
+
:whitespace CompilationLevel/WHITESPACE_ONLY
|
90
|
+
:simple CompilationLevel/SIMPLE_OPTIMIZATIONS)
|
91
|
+
compiler-options (doto (CompilerOptions.)
|
92
|
+
(.setCodingConvention (ClosureCodingConvention.)))]
|
93
|
+
(do (.setOptionsForCompilationLevel level compiler-options)
|
94
|
+
(set-options opts compiler-options)
|
95
|
+
compiler-options)))
|
96
|
+
|
97
|
+
(defn load-externs
|
98
|
+
"Externs are JavaScript files which contain empty definitions of
|
99
|
+
functions which will be provided by the envorinment. Any function in
|
100
|
+
an extern file will not be renamed during optimization.
|
101
|
+
|
102
|
+
Options may contain an :externs key with a list of file paths to
|
103
|
+
load. The :use-only-custom-externs flag may be used to indicate that
|
104
|
+
the default externs should be excluded."
|
105
|
+
[{:keys [externs use-only-custom-externs target]}]
|
106
|
+
(letfn [(filter-js [paths]
|
107
|
+
(for [p paths f (file-seq (io/file p))
|
108
|
+
:when (.endsWith (.toLowerCase (.getName f)) ".js")]
|
109
|
+
(.getAbsolutePath f)))
|
110
|
+
(add-target [ext]
|
111
|
+
(if (= :nodejs target)
|
112
|
+
(cons (.getFile (io/resource "cljs/nodejs_externs.js"))
|
113
|
+
(or ext []))
|
114
|
+
ext))
|
115
|
+
(load-js [ext]
|
116
|
+
(map #(js-source-file % (io/input-stream %)) ext))]
|
117
|
+
(let [js-sources (-> externs filter-js add-target load-js)]
|
118
|
+
(if use-only-custom-externs
|
119
|
+
js-sources
|
120
|
+
(into js-sources (CommandLineRunner/getDefaultExterns))))))
|
121
|
+
|
122
|
+
(defn ^com.google.javascript.jscomp.Compiler make-closure-compiler []
|
123
|
+
(let [compiler (com.google.javascript.jscomp.Compiler.)]
|
124
|
+
(do (com.google.javascript.jscomp.Compiler/setLoggingLevel Level/WARNING)
|
125
|
+
compiler)))
|
126
|
+
|
127
|
+
(defn report-failure [^Result result]
|
128
|
+
(let [errors (.errors result)
|
129
|
+
warnings (.warnings result)]
|
130
|
+
(doseq [next (seq errors)]
|
131
|
+
(println "ERROR:" (.toString ^JSError next)))
|
132
|
+
(doseq [next (seq warnings)]
|
133
|
+
(println "WARNING:" (.toString ^JSError next)))))
|
134
|
+
|
135
|
+
(defn parse-js-ns
|
136
|
+
"Given the lines from a JavaScript source file, parse the provide
|
137
|
+
and require statements and return them in a map. Assumes that all
|
138
|
+
provide and require statements appear before the first function
|
139
|
+
definition."
|
140
|
+
[lines]
|
141
|
+
(letfn [(conj-in [m k v] (update-in m [k] (fn [old] (conj old v))))]
|
142
|
+
(->> (for [line lines x (string/split line #";")] x)
|
143
|
+
(map string/trim)
|
144
|
+
(take-while #(not (re-matches #".*=[\s]*function\(.*\)[\s]*[{].*" %)))
|
145
|
+
(map #(re-matches #".*goog\.(provide|require)\('(.*)'\)" %))
|
146
|
+
(remove nil?)
|
147
|
+
(map #(drop 1 %))
|
148
|
+
(reduce (fn [m ns]
|
149
|
+
(if (= (first ns) "require")
|
150
|
+
(conj-in m :requires (last ns))
|
151
|
+
(conj-in m :provides (last ns))))
|
152
|
+
{:requires [] :provides []}))))
|
153
|
+
|
154
|
+
;; Protocols for IJavaScript and Compilable
|
155
|
+
;; ========================================
|
156
|
+
|
157
|
+
(defmulti to-url class)
|
158
|
+
|
159
|
+
(defmethod to-url File [^File f] (.toURL (.toURI f)))
|
160
|
+
|
161
|
+
(defmethod to-url String [s] (to-url (io/file s)))
|
162
|
+
|
163
|
+
(defprotocol IJavaScript
|
164
|
+
(-foreign? [this] "Whether the Javascript represents a foreign
|
165
|
+
library (a js file that not have any goog.provide statement")
|
166
|
+
(-url [this] "The URL where this JavaScript is located. Returns nil
|
167
|
+
when JavaScript exists in memory only.")
|
168
|
+
(-provides [this] "A list of namespaces that this JavaScript provides.")
|
169
|
+
(-requires [this] "A list of namespaces that this JavaScript requires.")
|
170
|
+
(-source [this] "The JavaScript source string."))
|
171
|
+
|
172
|
+
(extend-protocol IJavaScript
|
173
|
+
|
174
|
+
String
|
175
|
+
(-foreign? [this] false)
|
176
|
+
(-url [this] nil)
|
177
|
+
(-provides [this] (:provides (parse-js-ns (string/split-lines this))))
|
178
|
+
(-requires [this] (:requires (parse-js-ns (string/split-lines this))))
|
179
|
+
(-source [this] this)
|
180
|
+
|
181
|
+
clojure.lang.IPersistentMap
|
182
|
+
(-foreign? [this] (:foreign this))
|
183
|
+
(-url [this] (or (:url this)
|
184
|
+
(to-url (:file this))))
|
185
|
+
(-provides [this] (map name (:provides this)))
|
186
|
+
(-requires [this] (map name (:requires this)))
|
187
|
+
(-source [this] (if-let [s (:source this)]
|
188
|
+
s
|
189
|
+
(slurp (io/reader (-url this))))))
|
190
|
+
|
191
|
+
(defrecord JavaScriptFile [foreign ^URL url provides requires]
|
192
|
+
IJavaScript
|
193
|
+
(-foreign? [this] foreign)
|
194
|
+
(-url [this] url)
|
195
|
+
(-provides [this] provides)
|
196
|
+
(-requires [this] requires)
|
197
|
+
(-source [this] (slurp (io/reader url))))
|
198
|
+
|
199
|
+
(defn javascript-file [foreign ^URL url provides requires]
|
200
|
+
(JavaScriptFile. foreign url (map name provides) (map name requires)))
|
201
|
+
|
202
|
+
(defn map->javascript-file [m]
|
203
|
+
(javascript-file (:foreign m)
|
204
|
+
(to-url (:file m))
|
205
|
+
(:provides m)
|
206
|
+
(:requires m)))
|
207
|
+
|
208
|
+
(defn read-js
|
209
|
+
"Read a JavaScript file returning a map of file information."
|
210
|
+
[f]
|
211
|
+
(let [source (slurp f)
|
212
|
+
m (parse-js-ns (string/split-lines source))]
|
213
|
+
(map->javascript-file (assoc m :file f))))
|
214
|
+
|
215
|
+
(defprotocol Compilable
|
216
|
+
(-compile [this opts] "Returns one or more IJavaScripts."))
|
217
|
+
|
218
|
+
(defn build-index
|
219
|
+
"Index a list of dependencies by namespace and file name. There can
|
220
|
+
be zero or more namespaces provided per file."
|
221
|
+
[deps]
|
222
|
+
(reduce (fn [m next]
|
223
|
+
(let [provides (:provides next)]
|
224
|
+
(-> (if (seq provides)
|
225
|
+
(reduce (fn [m* provide]
|
226
|
+
(assoc m* provide next))
|
227
|
+
m
|
228
|
+
provides)
|
229
|
+
m)
|
230
|
+
(assoc (:file next) next))))
|
231
|
+
{}
|
232
|
+
deps))
|
233
|
+
|
234
|
+
(defn dependency-order-visit
|
235
|
+
[state ns-name]
|
236
|
+
(let [file (get state ns-name)]
|
237
|
+
(if (or (:visited file) (nil? file))
|
238
|
+
state
|
239
|
+
(let [state (assoc-in state [ns-name :visited] true)
|
240
|
+
deps (:requires file)
|
241
|
+
state (reduce dependency-order-visit state deps)]
|
242
|
+
(assoc state :order (conj (:order state) file))))))
|
243
|
+
|
244
|
+
(defn dependency-order
|
245
|
+
"Topologically sort a collection of dependencies."
|
246
|
+
[coll]
|
247
|
+
(let [state (build-index coll)]
|
248
|
+
(distinct (:order (reduce dependency-order-visit (assoc state :order []) (keys state))))))
|
249
|
+
|
250
|
+
;; Compile
|
251
|
+
;; =======
|
252
|
+
|
253
|
+
(defn empty-env []
|
254
|
+
{:ns (@comp/namespaces comp/*cljs-ns*) :context :statement :locals {}})
|
255
|
+
|
256
|
+
(defn compile-form-seq
|
257
|
+
"Compile a sequence of forms to a JavaScript source string."
|
258
|
+
[forms]
|
259
|
+
(comp/with-core-cljs
|
260
|
+
(with-out-str
|
261
|
+
(binding [comp/*cljs-ns* 'cljs.user]
|
262
|
+
(doseq [form forms]
|
263
|
+
(comp/emit (comp/analyze (empty-env) form)))))))
|
264
|
+
|
265
|
+
(defn output-directory [opts]
|
266
|
+
(or (:output-dir opts) "out"))
|
267
|
+
|
268
|
+
(def compiled-cljs (atom {}))
|
269
|
+
|
270
|
+
(defn compiled-file
|
271
|
+
"Given a map with at least a :file key, return a map with
|
272
|
+
{:file .. :provides .. :requires ..}.
|
273
|
+
|
274
|
+
Compiled files are cached so they will only be read once."
|
275
|
+
[m]
|
276
|
+
(let [path (.getAbsolutePath (:file m))
|
277
|
+
js (if (:provides m)
|
278
|
+
(map->javascript-file m)
|
279
|
+
(if-let [js (get @compiled-cljs path)]
|
280
|
+
js
|
281
|
+
(read-js (:file m))))]
|
282
|
+
(do (swap! compiled-cljs (fn [old] (assoc old path js)))
|
283
|
+
js)))
|
284
|
+
|
285
|
+
(defn compile-file
|
286
|
+
"Compile a single cljs file. If no output-file is specified, returns
|
287
|
+
a string of compiled JavaScript. With an output-file option, the
|
288
|
+
compiled JavaScript will written to this location and the function
|
289
|
+
returns a JavaScriptFile. In either case the return value satisfies
|
290
|
+
IJavaScript."
|
291
|
+
[^File file {:keys [output-file] :as opts}]
|
292
|
+
(if output-file
|
293
|
+
(let [out-file (io/file (output-directory opts) output-file)]
|
294
|
+
(compiled-file (comp/compile-file file out-file)))
|
295
|
+
(compile-form-seq (comp/forms-seq file))))
|
296
|
+
|
297
|
+
(defn compile-dir
|
298
|
+
"Recursively compile all cljs files under the given source
|
299
|
+
directory. Return a list of JavaScriptFiles in dependency order."
|
300
|
+
[^File src-dir opts]
|
301
|
+
(let [out-dir (output-directory opts)]
|
302
|
+
(dependency-order
|
303
|
+
(map compiled-file
|
304
|
+
(comp/compile-root src-dir out-dir)))))
|
305
|
+
|
306
|
+
(defn path-from-jarfile
|
307
|
+
"Given the URL of a file within a jar, return the path of the file
|
308
|
+
from the root of the jar."
|
309
|
+
[^URL url]
|
310
|
+
(last (string/split (.getFile url) #"\.jar!/")))
|
311
|
+
|
312
|
+
(defn jar-file-to-disk
|
313
|
+
"Copy a file contained within a jar to disk. Return the created file."
|
314
|
+
[url out-dir]
|
315
|
+
(let [out-file (io/file out-dir (path-from-jarfile url))
|
316
|
+
content (slurp (io/reader url))]
|
317
|
+
(do (comp/mkdirs out-file)
|
318
|
+
(spit out-file content)
|
319
|
+
out-file)))
|
320
|
+
|
321
|
+
(defn compile-from-jar
|
322
|
+
"Compile a file from a jar."
|
323
|
+
[this {:keys [output-file] :as opts}]
|
324
|
+
(or (when output-file
|
325
|
+
(let [out-file (io/file (output-directory opts) output-file)]
|
326
|
+
(when (.exists out-file)
|
327
|
+
(compiled-file {:file out-file}))))
|
328
|
+
(let [file-on-disk (jar-file-to-disk this (output-directory opts))]
|
329
|
+
(-compile file-on-disk opts))))
|
330
|
+
|
331
|
+
(extend-protocol Compilable
|
332
|
+
|
333
|
+
File
|
334
|
+
(-compile [this opts]
|
335
|
+
(if (.isDirectory this)
|
336
|
+
(compile-dir this opts)
|
337
|
+
(compile-file this opts)))
|
338
|
+
|
339
|
+
URL
|
340
|
+
(-compile [this opts]
|
341
|
+
(case (.getProtocol this)
|
342
|
+
"file" (-compile (io/file this) opts)
|
343
|
+
"jar" (compile-from-jar this opts)))
|
344
|
+
|
345
|
+
clojure.lang.PersistentList
|
346
|
+
(-compile [this opts]
|
347
|
+
(compile-form-seq [this]))
|
348
|
+
|
349
|
+
String
|
350
|
+
(-compile [this opts] (-compile (io/file this) opts))
|
351
|
+
|
352
|
+
clojure.lang.PersistentVector
|
353
|
+
(-compile [this opts] (compile-form-seq this))
|
354
|
+
)
|
355
|
+
|
356
|
+
(comment
|
357
|
+
;; compile a file in memory
|
358
|
+
(-compile "samples/hello/src/hello/core.cljs" {})
|
359
|
+
;; compile a file to disk - see file @ 'out/clojure/set.js'
|
360
|
+
(-compile (io/resource "clojure/set.cljs") {:output-file "clojure/set.js"})
|
361
|
+
;; compile a project
|
362
|
+
(-compile (io/file "samples/hello/src") {})
|
363
|
+
;; compile a project with a custom output directory
|
364
|
+
(-compile (io/file "samples/hello/src") {:output-dir "my-output"})
|
365
|
+
;; compile a form
|
366
|
+
(-compile '(defn plus-one [x] (inc x)) {})
|
367
|
+
;; compile a vector of forms
|
368
|
+
(-compile '[(ns test.app (:require [goog.array :as array]))
|
369
|
+
(defn plus-one [x] (inc x))]
|
370
|
+
{})
|
371
|
+
)
|
372
|
+
|
373
|
+
;; Dependencies
|
374
|
+
;; ============
|
375
|
+
;;
|
376
|
+
;; Find all dependencies from files on the classpath. Eliminates the
|
377
|
+
;; need for closurebuilder. cljs dependencies will be compiled as
|
378
|
+
;; needed.
|
379
|
+
|
380
|
+
(defn find-url
|
381
|
+
"Given a string, returns a URL. Attempts to resolve as a classpath-relative
|
382
|
+
path, then as a path relative to the working directory or a URL string"
|
383
|
+
[path-or-url]
|
384
|
+
(or (io/resource path-or-url)
|
385
|
+
(try (io/as-url path-or-url)
|
386
|
+
(catch java.net.MalformedURLException e
|
387
|
+
false))
|
388
|
+
(io/as-url (io/as-file path-or-url))))
|
389
|
+
|
390
|
+
(defn load-foreign-library*
|
391
|
+
"Given a library spec (a map containing the keys :file
|
392
|
+
and :provides), returns a map containing :provides, :requires, :file
|
393
|
+
and :url"
|
394
|
+
[lib-spec]
|
395
|
+
(merge lib-spec {:foreign true
|
396
|
+
:requires nil
|
397
|
+
:url (find-url (:file lib-spec))}))
|
398
|
+
|
399
|
+
(def load-foreign-library (memoize load-foreign-library*))
|
400
|
+
|
401
|
+
(defn load-library*
|
402
|
+
"Given a path to a JavaScript library, which is a directory
|
403
|
+
containing Javascript files, return a list of maps
|
404
|
+
containing :provides, :requires, :file and :url."
|
405
|
+
[path]
|
406
|
+
(letfn [(graph-node [f]
|
407
|
+
(-> (io/reader f)
|
408
|
+
line-seq
|
409
|
+
parse-js-ns
|
410
|
+
(assoc :file (.getPath f) :url (to-url f))))]
|
411
|
+
(let [js-sources (filter #(.endsWith (.getName %) ".js") (file-seq (io/file path)))]
|
412
|
+
(filter #(seq (:provides %)) (map graph-node js-sources)))))
|
413
|
+
|
414
|
+
(def load-library (memoize load-library*))
|
415
|
+
|
416
|
+
(defn library-dependencies [{:keys [libs foreign-libs]}]
|
417
|
+
(concat
|
418
|
+
(mapcat load-library libs)
|
419
|
+
(map load-foreign-library foreign-libs)))
|
420
|
+
|
421
|
+
(comment
|
422
|
+
;; load one library
|
423
|
+
(load-library* "closure/library/third_party/closure")
|
424
|
+
;; load all library dependencies
|
425
|
+
(library-dependencies {:libs ["closure/library/third_party/closure"]})
|
426
|
+
(library-dependencies {:foreign-libs [{:file "http://example.com/remote.js"
|
427
|
+
:provides ["my.example"]}]})
|
428
|
+
(library-dependencies {:foreign-libs [{:file "local/file.js"
|
429
|
+
:provides ["my.example"]}]})
|
430
|
+
(library-dependencies {:foreign-libs [{:file "cljs/nodejs_externs.js"
|
431
|
+
:provides ["my.example"]}]}))
|
432
|
+
|
433
|
+
(defn goog-dependencies*
|
434
|
+
"Create an index of Google dependencies by namespace and file name."
|
435
|
+
[]
|
436
|
+
(letfn [(parse-list [s] (when (> (count s) 0)
|
437
|
+
(-> (.substring s 1 (dec (count s)))
|
438
|
+
(string/split #"'\s*,\s*'"))))]
|
439
|
+
(->> (line-seq (io/reader (io/resource "goog/deps.js")))
|
440
|
+
(map #(re-matches #"^goog\.addDependency\(['\"](.*)['\"],\s*\[(.*)\],\s*\[(.*)\]\);.*" %))
|
441
|
+
(remove nil?)
|
442
|
+
(map #(drop 1 %))
|
443
|
+
(remove #(.startsWith (first %) "../../third_party"))
|
444
|
+
(map #(hash-map :file (str "goog/"(first %))
|
445
|
+
:provides (parse-list (second %))
|
446
|
+
:requires (parse-list (last %))
|
447
|
+
:group :goog)))))
|
448
|
+
|
449
|
+
(def goog-dependencies (memoize goog-dependencies*))
|
450
|
+
|
451
|
+
|
452
|
+
(defn js-dependency-index
|
453
|
+
"Returns the index for all JavaScript dependencies. Lookup by
|
454
|
+
namespace or file name."
|
455
|
+
[opts]
|
456
|
+
(build-index (concat (goog-dependencies) (library-dependencies opts))))
|
457
|
+
|
458
|
+
(defn js-dependencies
|
459
|
+
"Given a sequence of Closure namespace strings, return the list of
|
460
|
+
all dependencies in dependency order. The returned list includes all
|
461
|
+
Google and third-party library dependencies.
|
462
|
+
|
463
|
+
Third-party libraries are configured using the :libs option where
|
464
|
+
the value is a list of directories containing third-party
|
465
|
+
libraries."
|
466
|
+
[opts requires]
|
467
|
+
(let [index (js-dependency-index opts)]
|
468
|
+
(loop [requires requires
|
469
|
+
visited requires
|
470
|
+
deps #{}]
|
471
|
+
(if (seq requires)
|
472
|
+
(let [node (get index (first requires))
|
473
|
+
new-req (remove #(contains? visited %) (:requires node))]
|
474
|
+
(recur (into (rest requires) new-req)
|
475
|
+
(into visited new-req)
|
476
|
+
(conj deps node)))
|
477
|
+
(cons (get index "goog/base.js") (dependency-order deps))))))
|
478
|
+
|
479
|
+
(comment
|
480
|
+
;; find dependencies
|
481
|
+
(js-dependencies {} ["goog.array"])
|
482
|
+
;; find dependencies in an external library
|
483
|
+
(js-dependencies {:libs ["closure/library/third_party/closure"]} ["goog.dom.query"])
|
484
|
+
)
|
485
|
+
|
486
|
+
(defn get-compiled-cljs
|
487
|
+
"Return an IJavaScript for this file. Compiled output will be
|
488
|
+
written to the working directory."
|
489
|
+
[opts {:keys [relative-path uri]}]
|
490
|
+
(let [js-file (comp/rename-to-js relative-path)]
|
491
|
+
(-compile uri (merge opts {:output-file js-file}))))
|
492
|
+
|
493
|
+
(defn cljs-dependencies
|
494
|
+
"Given a list of all required namespaces, return a list of
|
495
|
+
IJavaScripts which are the cljs dependencies in dependency
|
496
|
+
order. The returned list will not only include the explicitly
|
497
|
+
required files but any transitive depedencies as well. JavaScript
|
498
|
+
files will be compiled to the working directory if they do not
|
499
|
+
already exist.
|
500
|
+
|
501
|
+
Only load dependencies from the classpath."
|
502
|
+
[opts requires]
|
503
|
+
(let [index (js-dependency-index opts)]
|
504
|
+
(letfn [(ns->cp [s] (str (string/replace (munge s) \. \/) ".cljs"))
|
505
|
+
(cljs-deps [coll]
|
506
|
+
(->> coll
|
507
|
+
(remove #(contains? index %))
|
508
|
+
(map #(let [f (ns->cp %)] (hash-map :relative-path f :uri (io/resource f))))
|
509
|
+
(remove #(nil? (:uri %)))))]
|
510
|
+
(loop [required-files (cljs-deps requires)
|
511
|
+
visited (set required-files)
|
512
|
+
js-deps #{}]
|
513
|
+
(if (seq required-files)
|
514
|
+
(let [next-file (first required-files)
|
515
|
+
js (get-compiled-cljs opts next-file)
|
516
|
+
new-req (remove #(contains? visited %) (cljs-deps (-requires js)))]
|
517
|
+
(recur (into (rest required-files) new-req)
|
518
|
+
(into visited new-req)
|
519
|
+
(conj js-deps js)))
|
520
|
+
(dependency-order js-deps))))))
|
521
|
+
|
522
|
+
(comment
|
523
|
+
;; only get cljs deps
|
524
|
+
(cljs-dependencies {} ["goog.string" "cljs.core"])
|
525
|
+
;; get transitive deps
|
526
|
+
(cljs-dependencies {} ["clojure.string"])
|
527
|
+
;; don't get cljs.core twice
|
528
|
+
(cljs-dependencies {} ["cljs.core" "clojure.string"])
|
529
|
+
)
|
530
|
+
|
531
|
+
(defn add-dependencies
|
532
|
+
"Given one or more IJavaScript objects in dependency order, produce
|
533
|
+
a new sequence of IJavaScript objects which includes the input list
|
534
|
+
plus all dependencies in dependency order."
|
535
|
+
[opts & inputs]
|
536
|
+
(let [requires (mapcat -requires inputs)
|
537
|
+
required-cljs (cljs-dependencies opts requires)
|
538
|
+
required-js (js-dependencies opts (set (concat (mapcat -requires required-cljs) requires)))]
|
539
|
+
(concat (map #(-> (javascript-file (:foreign %)
|
540
|
+
(or (:url %) (io/resource (:file %)))
|
541
|
+
(:provides %)
|
542
|
+
(:requires %))
|
543
|
+
(assoc :group (:group %))) required-js)
|
544
|
+
required-cljs
|
545
|
+
inputs)))
|
546
|
+
|
547
|
+
(comment
|
548
|
+
;; add dependencies to literal js
|
549
|
+
(add-dependencies {} "goog.provide('test.app');\ngoog.require('cljs.core');")
|
550
|
+
(add-dependencies {} "goog.provide('test.app');\ngoog.require('goog.array');")
|
551
|
+
(add-dependencies {} (str "goog.provide('test.app');\n"
|
552
|
+
"goog.require('goog.array');\n"
|
553
|
+
"goog.require('clojure.set');"))
|
554
|
+
;; add dependencies with external lib
|
555
|
+
(add-dependencies {:libs ["closure/library/third_party/closure"]}
|
556
|
+
(str "goog.provide('test.app');\n"
|
557
|
+
"goog.require('goog.array');\n"
|
558
|
+
"goog.require('goog.dom.query');"))
|
559
|
+
;; add dependencies with foreign lib
|
560
|
+
(add-dependencies {:foreign-libs [{:file "samples/hello/src/hello/core.cljs"
|
561
|
+
:provides ["example.lib"]}]}
|
562
|
+
(str "goog.provide('test.app');\n"
|
563
|
+
"goog.require('example.lib');\n"))
|
564
|
+
;; add dependencies to a JavaScriptFile record
|
565
|
+
(add-dependencies {} (javascript-file false
|
566
|
+
(to-url "samples/hello/src/hello/core.cljs")
|
567
|
+
["hello.core"]
|
568
|
+
["goog.array"]))
|
569
|
+
)
|
570
|
+
|
571
|
+
;; Optimize
|
572
|
+
;; ========
|
573
|
+
|
574
|
+
(defmulti javascript-name class)
|
575
|
+
|
576
|
+
(defmethod javascript-name URL [^URL url]
|
577
|
+
(if url (.getPath url) "cljs/user.js"))
|
578
|
+
|
579
|
+
(defmethod javascript-name String [s]
|
580
|
+
(if-let [name (first (-provides s))] name "cljs/user.js"))
|
581
|
+
|
582
|
+
(defmethod javascript-name JavaScriptFile [js] (javascript-name (-url js)))
|
583
|
+
|
584
|
+
(defn build-provides
|
585
|
+
"Given a vector of provides, builds required goog.provide statements"
|
586
|
+
[provides]
|
587
|
+
(apply str (map #(str "goog.provide('" % "');\n") provides)))
|
588
|
+
|
589
|
+
|
590
|
+
(defmethod js-source-file JavaScriptFile [_ js]
|
591
|
+
(when-let [url (-url js)]
|
592
|
+
(js-source-file (javascript-name url)
|
593
|
+
(if (-foreign? js)
|
594
|
+
(str (build-provides (-provides js)) (slurp url))
|
595
|
+
(io/input-stream url)))))
|
596
|
+
|
597
|
+
(defn optimize
|
598
|
+
"Use the Closure Compiler to optimize one or more JavaScript files."
|
599
|
+
[opts & sources]
|
600
|
+
(let [closure-compiler (make-closure-compiler)
|
601
|
+
externs (load-externs opts)
|
602
|
+
compiler-options (make-options opts)
|
603
|
+
inputs (map #(js-source-file (javascript-name %) %) sources)
|
604
|
+
result ^Result (.compile closure-compiler externs inputs compiler-options)]
|
605
|
+
(if (.success result)
|
606
|
+
(.toSource closure-compiler)
|
607
|
+
(report-failure result))))
|
608
|
+
|
609
|
+
(comment
|
610
|
+
;; optimize JavaScript strings
|
611
|
+
(optimize {:optimizations :whitespace} "var x = 3 + 2; alert(x);")
|
612
|
+
;; => "var x=3+2;alert(x);"
|
613
|
+
(optimize {:optimizations :simple} "var x = 3 + 2; alert(x);")
|
614
|
+
;; => "var x=5;alert(x);"
|
615
|
+
(optimize {:optimizations :advanced} "var x = 3 + 2; alert(x);")
|
616
|
+
;; => "alert(5);"
|
617
|
+
|
618
|
+
;; optimize a ClojureScript form
|
619
|
+
(optimize {:optimizations :simple} (-compile '(def x 3) {}))
|
620
|
+
|
621
|
+
;; optimize a project
|
622
|
+
(println (->> (-compile "samples/hello/src" {})
|
623
|
+
(apply add-dependencies {})
|
624
|
+
(apply optimize {:optimizations :simple :pretty-print true})))
|
625
|
+
)
|
626
|
+
|
627
|
+
;; Output
|
628
|
+
;; ======
|
629
|
+
;;
|
630
|
+
;; The result of a build is always a single string of JavaScript. The
|
631
|
+
;; build process may produce files on disk but a single string is
|
632
|
+
;; always output. What this string contains depends on whether the
|
633
|
+
;; input has been optimized or not. If the :output-to option is set
|
634
|
+
;; then this string will be written to the specified file. If not, it
|
635
|
+
;; will be returned.
|
636
|
+
;;
|
637
|
+
;; The :output-dir option can be used to set the working directory
|
638
|
+
;; where any files will be written to disk. By default this directory
|
639
|
+
;; is 'out'.
|
640
|
+
;;
|
641
|
+
;; If inputs are optimized then the output string will be the complete
|
642
|
+
;; application with all dependencies included.
|
643
|
+
;;
|
644
|
+
;; For unoptimized output, the string will be a Closure deps file
|
645
|
+
;; describing where the JavaScript files are on disk and their
|
646
|
+
;; dependencies. All JavaScript files will be located in the working
|
647
|
+
;; directory, including any dependencies from the Closure library.
|
648
|
+
;;
|
649
|
+
;; Unoptimized mode is faster because the Closure Compiler is not
|
650
|
+
;; run. It also makes debugging much simpler because each file is
|
651
|
+
;; loaded in its own script tag.
|
652
|
+
;;
|
653
|
+
;; When working with uncompiled files, you will need to add additional
|
654
|
+
;; script tags to the hosting HTML file: one which pulls in Closure
|
655
|
+
;; library's base.js and one which calls goog.require to load your
|
656
|
+
;; code. See samples/hello/hello-dev.html for an example.
|
657
|
+
|
658
|
+
(defn path-relative-to
|
659
|
+
"Generate a string which is the path to input relative to base."
|
660
|
+
[^File base input]
|
661
|
+
(let [base-path (comp/path-seq (.getCanonicalPath base))
|
662
|
+
input-path (comp/path-seq (.getCanonicalPath (io/file ^URL (-url input))))
|
663
|
+
count-base (count base-path)
|
664
|
+
common (count (take-while true? (map #(= %1 %2) base-path input-path)))
|
665
|
+
prefix (repeat (- count-base common 1) "..")]
|
666
|
+
(if (= count-base common)
|
667
|
+
(last input-path) ;; same file
|
668
|
+
(comp/to-path (concat prefix (drop common input-path)) "/"))))
|
669
|
+
|
670
|
+
(defn add-dep-string
|
671
|
+
"Return a goog.addDependency string for an input."
|
672
|
+
[opts input]
|
673
|
+
(letfn [(ns-list [coll] (when (seq coll) (apply str (interpose ", " (map #(str "'" (munge %) "'") coll)))))]
|
674
|
+
(str "goog.addDependency(\""
|
675
|
+
(path-relative-to (io/file (output-directory opts) "goog/base.js") input)
|
676
|
+
"\", ["
|
677
|
+
(ns-list (-provides input))
|
678
|
+
"], ["
|
679
|
+
(ns-list (-requires input))
|
680
|
+
"]);")))
|
681
|
+
|
682
|
+
(defn deps-file
|
683
|
+
"Return a deps file string for a sequence of inputs."
|
684
|
+
[opts sources]
|
685
|
+
(apply str (interpose "\n" (map #(add-dep-string opts %) sources))))
|
686
|
+
|
687
|
+
(comment
|
688
|
+
(path-relative-to (io/file "out/goog/base.js") {:url (to-url "out/cljs/core.js")})
|
689
|
+
(add-dep-string {} {:url (to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]})
|
690
|
+
(deps-file {} [{:url (to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]}])
|
691
|
+
)
|
692
|
+
|
693
|
+
(defn output-one-file [{:keys [output-to]} js]
|
694
|
+
(cond (nil? output-to) js
|
695
|
+
(string? output-to) (spit output-to js)
|
696
|
+
:else (println js)))
|
697
|
+
|
698
|
+
(defn output-deps-file [opts sources]
|
699
|
+
(output-one-file opts (deps-file opts sources)))
|
700
|
+
|
701
|
+
(defn ^String output-path
|
702
|
+
"Given an IJavaScript which is either in memory or in a jar file,
|
703
|
+
return the output path for this file relative to the working
|
704
|
+
directory."
|
705
|
+
[js]
|
706
|
+
(if-let [url ^URL (-url js)]
|
707
|
+
(path-from-jarfile url)
|
708
|
+
(str (random-string 5) ".js")))
|
709
|
+
|
710
|
+
|
711
|
+
(defn write-javascript
|
712
|
+
"Write a JavaScript file to disk. Only write if the file does not
|
713
|
+
already exist. Return IJavaScript for the file on disk."
|
714
|
+
[opts js]
|
715
|
+
(let [out-dir (io/file (output-directory opts))
|
716
|
+
out-name (output-path js)
|
717
|
+
out-file (io/file out-dir out-name)]
|
718
|
+
(do (when-not (.exists out-file)
|
719
|
+
(do (comp/mkdirs out-file)
|
720
|
+
(spit out-file (-source js))))
|
721
|
+
{:url (to-url out-file) :requires (-requires js) :provides (-provides js) :group (:group js)})))
|
722
|
+
|
723
|
+
(defn source-on-disk
|
724
|
+
"Ensure that the given JavaScript exists on disk. Write in memory
|
725
|
+
sources and files contained in jars to the working directory. Return
|
726
|
+
updated IJavaScript with the new location."
|
727
|
+
[opts js]
|
728
|
+
(let [url ^URL (-url js)]
|
729
|
+
(if (or (not url) (= (.getProtocol url) "jar"))
|
730
|
+
(write-javascript opts js)
|
731
|
+
js)))
|
732
|
+
|
733
|
+
(comment
|
734
|
+
(write-javascript {} "goog.provide('demo');\nalert('hello');\n")
|
735
|
+
;; write something from a jar file to disk
|
736
|
+
(source-on-disk {}
|
737
|
+
{:url (io/resource "goog/base.js")
|
738
|
+
:source (slurp (io/reader (io/resource "goog/base.js")))})
|
739
|
+
;; doesn't write a file that is already on disk
|
740
|
+
(source-on-disk {} {:url (io/resource "cljs/core.cljs")})
|
741
|
+
)
|
742
|
+
|
743
|
+
(defn output-unoptimized
|
744
|
+
"Ensure that all JavaScript source files are on disk (not in jars),
|
745
|
+
write the goog deps file including only the libraries that are being
|
746
|
+
used and write the deps file for the current project.
|
747
|
+
|
748
|
+
The deps file for the current project will include third-party
|
749
|
+
libraries."
|
750
|
+
[opts & sources]
|
751
|
+
(let [disk-sources (map #(source-on-disk opts %) sources)]
|
752
|
+
(let [goog-deps (io/file (output-directory opts) "goog/deps.js")]
|
753
|
+
(do (comp/mkdirs goog-deps)
|
754
|
+
(spit goog-deps (deps-file opts (filter #(= (:group %) :goog) disk-sources)))
|
755
|
+
(output-deps-file opts (remove #(= (:group %) :goog) disk-sources))))))
|
756
|
+
|
757
|
+
(comment
|
758
|
+
|
759
|
+
;; output unoptimized alone
|
760
|
+
(output-unoptimized {} "goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n")
|
761
|
+
;; output unoptimized with all dependencies
|
762
|
+
(apply output-unoptimized {}
|
763
|
+
(add-dependencies {}
|
764
|
+
"goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n"))
|
765
|
+
;; output unoptimized with external library
|
766
|
+
(apply output-unoptimized {}
|
767
|
+
(add-dependencies {:libs ["closure/library/third_party/closure"]}
|
768
|
+
"goog.provide('test');\ngoog.require('cljs.core');\ngoog.require('goog.dom.query');\n"))
|
769
|
+
;; output unoptimized and write deps file to 'out/test.js'
|
770
|
+
(output-unoptimized {:output-to "out/test.js"}
|
771
|
+
"goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n")
|
772
|
+
)
|
773
|
+
|
774
|
+
(defn add-header [{:keys [hashbang target]} js]
|
775
|
+
(if (= :nodejs target)
|
776
|
+
(str "#!" (or hashbang "/usr/bin/nodejs") "\n" js)
|
777
|
+
js))
|
778
|
+
|
779
|
+
(defn build
|
780
|
+
"Given a source which can be compiled, produce runnable JavaScript."
|
781
|
+
[source opts]
|
782
|
+
(let [opts (if (= :nodejs (:target opts))
|
783
|
+
(merge {:optimizations :simple} opts)
|
784
|
+
opts)
|
785
|
+
compiled (-compile source opts)
|
786
|
+
compiled (concat
|
787
|
+
(if (coll? compiled) compiled [compiled])
|
788
|
+
(when (= :nodejs (:target opts))
|
789
|
+
[(-compile (io/resource "cljs/nodejscli.cljs") opts)]))
|
790
|
+
js-sources (if (coll? compiled)
|
791
|
+
(apply add-dependencies opts compiled)
|
792
|
+
(add-dependencies opts compiled))]
|
793
|
+
(if (:optimizations opts)
|
794
|
+
(->> js-sources
|
795
|
+
(apply optimize opts)
|
796
|
+
(add-header opts)
|
797
|
+
(output-one-file opts))
|
798
|
+
(apply output-unoptimized opts js-sources))))
|
799
|
+
|
800
|
+
(comment
|
801
|
+
|
802
|
+
(println (build '[(ns hello.core)
|
803
|
+
(defn ^{:export greet} greet [n] (str "Hola " n))
|
804
|
+
(defn ^:export sum [xs] 42)]
|
805
|
+
{:optimizations :simple :pretty-print true}))
|
806
|
+
|
807
|
+
;; build a project with optimizations
|
808
|
+
(build "samples/hello/src" {:optimizations :advanced})
|
809
|
+
(build "samples/hello/src" {:optimizations :advanced :output-to "samples/hello/hello.js"})
|
810
|
+
;; open 'samples/hello/hello.html' to see the result in action
|
811
|
+
|
812
|
+
;; build a project without optimizations
|
813
|
+
(build "samples/hello/src" {:output-dir "samples/hello/out" :output-to "samples/hello/hello.js"})
|
814
|
+
;; open 'samples/hello/hello-dev.html' to see the result in action
|
815
|
+
;; notice how each script was loaded individually
|
816
|
+
|
817
|
+
;; build unoptimized from raw ClojureScript
|
818
|
+
(build '[(ns hello.core)
|
819
|
+
(defn ^{:export greet} greet [n] (str "Hola " n))
|
820
|
+
(defn ^:export sum [xs] 42)]
|
821
|
+
{:output-dir "samples/hello/out" :output-to "samples/hello/hello.js"})
|
822
|
+
;; open 'samples/hello/hello-dev.html' to see the result in action
|
823
|
+
)
|