Skip to content

Commit

Permalink
Merge pull request #1881 from opal/elia/remove-hike-dependency
Browse files Browse the repository at this point in the history
Embed hike-1.2.3 into Opal
  • Loading branch information
elia committed Sep 4, 2018
2 parents 375beda + fb63e49 commit 0bcce30
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 11 deletions.
305 changes: 305 additions & 0 deletions lib/opal/hike.rb
@@ -0,0 +1,305 @@
# frozen_string_literal: true

# Copyright (c) 2011 Sam Stephenson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'pathname'

module Opal
# Taken from hike v1.2.3
module Hike
# `Index` is an internal cached variant of `Trail`. It assumes the
# file system does not change between `find` calls. All `stat` and
# `entries` calls are cached for the lifetime of the `Index` object.
class Index
# `Index#paths` is an immutable collection of `Pathname`s.
attr_reader :paths

# `Index#extensions` is an immutable collection of extensions.
attr_reader :extensions

# `Index.new` is an internal method. Instead of constructing it
# directly, create a `Trail` and call `Trail#index`.
def initialize(root, paths, extensions)
@root = root

# Freeze is used here so an error is throw if a mutator method
# is called on the array. Mutating `@paths`, `@extensions`
# would have unpredictable results.
@paths = paths.dup.freeze
@extensions = extensions.dup.freeze
@pathnames = paths.map { |path| Pathname.new(path) }

@stats = {}
@entries = {}
@patterns = {}
end

# `Index#root` returns root path as a `String`. This attribute is immutable.
def root
@root.to_s
end

# `Index#index` returns `self` to be compatable with the `Trail` interface.
def index
self
end

# The real implementation of `find`. `Trail#find` generates a one
# time index and delegates here.
#
# See `Trail#find` for usage.
def find(logical_path)
base_path = Pathname.new(@root)
logical_path = Pathname.new(logical_path.sub(/^\//, ''))

if logical_path.to_s =~ %r{^\.\.?/}
find_in_base_path(logical_path, base_path) { |path| return path }
else
find_in_paths(logical_path) { |path| return path }
end

nil
end

# A cached version of `Dir.entries` that filters out `.` files and
# `~` swap files. Returns an empty `Array` if the directory does
# not exist.
def entries(path)
@entries[path.to_s] ||= begin
pathname = Pathname.new(path)
if pathname.directory?
pathname.entries.reject { |entry| entry.to_s =~ /^\.|~$|^\#.*\#$/ }.sort
else
[]
end
end
end

# A cached version of `File.stat`. Returns nil if the file does
# not exist.
def stat(path)
key = path.to_s
if @stats.key?(key)
@stats[key]
elsif File.exist?(path)
@stats[key] = File.stat(path)
else
@stats[key] = nil
end
end

protected

def extract_options!(arguments)
arguments.last.is_a?(Hash) ? arguments.pop.dup : {}
end

# Finds logical path across all `paths`
def find_in_paths(logical_path, &block)
dirname, basename = logical_path.split
@pathnames.each do |base_path|
match(base_path.join(dirname), basename, &block)
end
end

# Finds relative logical path, `../test/test_trail`. Requires a
# `base_path` for reference.
def find_in_base_path(logical_path, base_path, &block)
candidate = base_path.join(logical_path)
dirname, basename = candidate.split
match(dirname, basename, &block) if paths_contain?(dirname)
end

# Checks if the path is actually on the file system and performs
# any syscalls if necessary.
def match(dirname, basename)
# Potential `entries` syscall
matches = entries(dirname)

pattern = pattern_for(basename)
matches = matches.select { |m| m.to_s =~ pattern }

sort_matches(matches, basename).each do |path|
pathname = dirname.join(path)

# Potential `stat` syscall
stat = stat(pathname)

# Exclude directories
if stat && stat.file?
yield pathname.to_s
end
end
end

# Returns true if `dirname` is a subdirectory of any of the `paths`
def paths_contain?(dirname)
paths.any? { |path| dirname.to_s[0, path.length] == path }
end

# Cache results of `build_pattern_for`
def pattern_for(basename)
@patterns[basename] ||= build_pattern_for(basename)
end

# Returns a `Regexp` that matches the allowed extensions.
#
# pattern_for("index.html") #=> /^index(.html|.htm)(.builder|.erb)*$/
def build_pattern_for(basename)
extension_pattern = extensions.map { |e| Regexp.escape(e) }.join('|')
/^#{basename}(?:#{extension_pattern})*$/
end

# Sorts candidate matches by their extension
# priority. Extensions in the front of the `extensions` carry
# more weight.
def sort_matches(matches, basename)
matches.sort_by do |match|
extnames = match.sub(basename.to_s, '').to_s.scan(/\.[^.]+/)
extnames.inject(0) do |sum, ext|
index = extensions.index(ext)
if index
sum + index + 1
else
sum
end
end
end
end
end

# `Trail` is the public container class for holding paths and extensions.
class Trail
# `Trail#paths` is a mutable `Paths` collection.
#
# trail = Hike::Trail.new
# trail.paths.push "~/Projects/hike/lib", "~/Projects/hike/test"
#
# The order of the paths is significant. Paths in the beginning of
# the collection will be checked first. In the example above,
# `~/Projects/hike/lib/hike.rb` would shadow the existent of
# `~/Projects/hike/test/hike.rb`.
attr_reader :paths

# `Trail#extensions` is a mutable `Extensions` collection.
#
# trail = Hike::Trail.new
# trail.paths.push "~/Projects/hike/lib"
# trail.extensions.push ".rb"
#
# Extensions allow you to find files by just their name omitting
# their extension. Is similar to Ruby's require mechanism that
# allows you to require files with specifiying `foo.rb`.
attr_reader :extensions

# A Trail accepts an optional root path that defaults to your
# current working directory. Any relative paths added to
# `Trail#paths` will expanded relative to the root.
def initialize(root = '.')
@root = Pathname.new(root).expand_path
@paths = []
@extensions = []
end

# `Trail#root` returns root path as a `String`. This attribute is immutable.
def root
@root.to_s
end

# Append `path` to `Paths` collection
def append_paths(*paths)
@paths.concat(paths.map { |p| normalize_path(p) })
end

# Append `extension` to `Extensions` collection
def append_extensions(*extensions)
@extensions.concat(extensions.map { |e| normalize_extension(e) })
end

# `Trail#find` returns a the expand path for a logical path in the
# path collection.
#
# trail = Hike::Trail.new "~/Projects/hike"
# trail.extensions.push ".rb"
# trail.paths.push "lib", "test"
#
# trail.find "hike/trail"
# # => "~/Projects/hike/lib/hike/trail.rb"
#
# trail.find "test_trail"
# # => "~/Projects/hike/test/test_trail.rb"
#
def find(*args, &block)
index.find(*args, &block)
end

# `Trail#index` returns an `Index` object that has the same
# interface as `Trail`. An `Index` is a cached `Trail` object that
# does not update when the file system changes. If you are
# confident that you are not making changes the paths you are
# searching, `index` will avoid excess system calls.
#
# index = trail.index
# index.find "hike/trail"
# index.find "test_trail"
#
def index
Index.new(root, paths, extensions)
end

# `Trail#entries` is equivalent to `Dir#entries`. It is not
# recommend to use this method for general purposes. It exists for
# parity with `Index#entries`.
def entries(path)
pathname = Pathname.new(path)
if pathname.directory?
pathname.entries.reject { |entry| entry.to_s =~ /^\.|~$|^\#.*\#$/ }.sort
else
[]
end
end

# `Trail#stat` is equivalent to `File#stat`. It is not
# recommend to use this method for general purposes. It exists for
# parity with `Index#stat`.
def stat(path)
if File.exist?(path)
File.stat(path.to_s)
else
# nil
end
end

private

def normalize_extension(ext)
ext.start_with?('.') ? ext : ".#{ext}"
end

def normalize_path(path)
path = Pathname.new(path)
path = @root.join(path) if path.relative?
path.expand_path.to_s
end
end
end
end
2 changes: 1 addition & 1 deletion lib/opal/path_reader.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require 'opal/regexp_anchors'
require 'hike'
require 'opal/hike'

module Opal
class PathReader
Expand Down
1 change: 0 additions & 1 deletion opal.gemspec
Expand Up @@ -35,7 +35,6 @@ Gem::Specification.new do |spec|

spec.required_ruby_version = '>= 2.0.0'

spec.add_dependency 'hike', '~> 1.2'
spec.add_dependency 'ast', '>= 2.3.0'
spec.add_dependency 'parser', '= 2.5.1.0'

Expand Down
4 changes: 2 additions & 2 deletions spec/lib/path_reader_spec.rb
Expand Up @@ -8,8 +8,8 @@
let(:contents) { File.read(full_path) }

before do
allow_any_instance_of(Hike::Trail).to receive(:find) {|path| nil}
allow_any_instance_of(Hike::Trail).to receive(:find).with(path).and_return(full_path)
allow_any_instance_of(Opal::Hike::Trail).to receive(:find) {|path| nil}
allow_any_instance_of(Opal::Hike::Trail).to receive(:find).with(path).and_return(full_path)
end

describe '#paths' do
Expand Down
3 changes: 0 additions & 3 deletions tasks/building.rake
Expand Up @@ -19,9 +19,6 @@ task :dist do
Opal::Config.dynamic_require_severity = :warning
Opal::Config.missing_require_severity = :error

# Hike gem is required to build opal-builder
Opal.use_gem 'hike'

build_dir = ENV['DIR'] || 'build'
files = ENV['FILES'] ? ENV['FILES'].split(',') :
Dir['{opal,stdlib}/*.rb'].map { |lib| File.basename(lib, '.rb') }
Expand Down
6 changes: 2 additions & 4 deletions tasks/testing.rake
Expand Up @@ -329,7 +329,7 @@ platforms.each do |platform|
includes = "-Itest -Ilib -Ivendored-minitest #{includes}"

sh "ruby -rbundler/setup "\
"exe/opal -ghike #{includes} #{stubs} -R#{platform} -Dwarning -A --enable-source-location #{filename}"
"exe/opal #{includes} #{stubs} -R#{platform} -Dwarning -A --enable-source-location #{filename}"
end
end
end
Expand All @@ -338,8 +338,6 @@ end
# The name ends with the platform, which is of course mandated in this case
desc "Run the Node.js Minitest suite on Node.js"
task :minitest_node_nodejs do
Opal.use_gem 'hike'

platform = 'nodejs'
files = %w[
nodejs
Expand All @@ -357,7 +355,7 @@ task :minitest_node_nodejs do
includes = "-Itest -Ilib -Ivendored-minitest"

sh "ruby -rbundler/setup "\
"exe/opal -ghike #{includes} #{stubs} -R#{platform} -Dwarning -A --enable-source-location #{filename}"
"exe/opal #{includes} #{stubs} -R#{platform} -Dwarning -A --enable-source-location #{filename}"
end

desc 'Run smoke tests with opal-rspec to see if something is broken'
Expand Down

0 comments on commit 0bcce30

Please sign in to comment.