require 'typescript/monkey'
require 'typescript/monkey/package'
module Typescript::Monkey::Compiler
class TypescriptCompileError < RuntimeError; end
class << self
# Replace relative paths specified in /// with absolute paths.
#
# @param [String] ts_path Source .ts path
# @param [String] source. It might be pre-processed by erb.
# @return [String] replaces source
#
def replace_relative_references(ts_path, source)
ts_dir = File.dirname(File.expand_path(ts_path))
escaped_dir = ts_dir.gsub(/["\\]/, '\\\\\&') # "\"" => "\\\"", '\\' => '\\\\'
# Why don't we just use gsub? Because it display odd behavior with File.join on Ruby 2.0
# So we go the long way around.
(source.each_line.map do |l|
if l.starts_with?('///') && !(m = %r!^///\s*\s*!.match(l)).nil?
matched_path = m.captures.compact[0]
l = l.sub(matched_path, File.join(escaped_dir, matched_path))
end
next l
end).join
end
# Get all references
#
# @param [String] path Source .ts path
# @param [String] source. It might be pre-processed by erb.
# @yieldreturn [String] matched ref abs_path
#
def get_all_reference_paths(path, source, visited_paths=Set.new, &block)
visited_paths << path
source ||= File.read(path)
source.each_line do |l|
if l.starts_with?('///') && !(m = %r!^///\s*\s*!.match(l)).nil?
matched_path = m.captures.compact[0]
abs_matched_path = File.expand_path(matched_path, File.dirname(path))
unless visited_paths.include? abs_matched_path
block.call abs_matched_path
get_all_reference_paths(abs_matched_path, nil, visited_paths, &block)
end
end
end
end
# Compile source
#
# @param [String] ts_path
# @param [String] source TypeScript source code
# @param [Sprockets::Context] sprockets context object
# @return [String] compiled JavaScript source code
#
def compile(ts_path, source, context=nil, *options)
if context
get_all_reference_paths(File.expand_path(ts_path), source) do |abs_path|
context.depend_on abs_path
end
end
begin
command_path = Typescript::Monkey::Package.compiler_bin()
if command_path.nil?
raise RuntimeError, "Failed to find typescript compiler in local or global node environment."
end
log("#{module_name} processing: #{ts_path}")
# compile file
s = replace_relative_references(ts_path, source)
source_file = Tempfile.new(["typescript-monkey", ".ts"])
source_file.write(s)
source_file.close
args = Typescript::Monkey.configuration.options.map(&:dup)
# _args = [ "--out /dev/stdout", "--noResolve" ]
# if self.tsconfig && File.exist?(self.tsconfig)
# _args.push("--project #{self.tsconfig}")
# end
args.push(source_file.path)
compiled_source, _, status = Typescript::Monkey::CLI.run_command(command_path, args)
filtered_output = nil
# Parse errors from output: there is no way (currently) to suppress the
# errors emitted when passing --noResolve argument to tsc.
#
# Status values:
# Success = 0
# DiagnosticsPresent_OutputsSkipped = 1
# DiagnosticsPresent_OutputsGenerated = 2
#
# See: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts
#
# Ignore the following error codes:
# TS2304: Cannot find name ...
# TS2307: Cannot find module ...
# TS2318: Cannot find global type ...
# TS2339: Property ... does not exist on type ... **
# TS2468: Cannot find global value ...
# TS2503: Cannot find namespace ...
# TS2662: Cannot find name ... Did you mean the static member ...
# TS2663: Cannot find name ... Did you mean the instance member ...
# TS2688: Cannot find type definition file for ...
# TS2694: Namespace ... has no exported member ... **
#
# See: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/diagnosticMessages.json
#
unless status.success?
filtered_output = ""
ignore_errors = [
"TS2304",
"TS2307",
"TS2318",
"TS2339",
"TS2468",
"TS2503",
"TS2662",
"TS2663",
"TS2688",
"TS2694"
]
regex = /#{Regexp.escape(File.basename(source_file))}\(([\d]+,[\d]+)\):[\s]+error[\s]+(TS[\d]+):[\s]+(.*)$/
errors = []
compiled_source.split("\n").each do |line|
if (matches = line.match(regex))
errors << {
code: matches[2],
message: matches[3],
line: line,
line_position: matches[1]
}
next
end
filtered_output << line << "\n"
end
# iterate over errors and log ignored, raise exception for all others
errors.each do |error|
log("#{module_name} parsing error for file: #{ts_path}, #{error[:code]}@(#{error[:line_position]}): #{error[:message]}")
unless ignore_errors.include?(error[:code])
raise TypescriptCompileError, "#{error[:code]}@(#{error[:line_position]}): #{error[:message]}"
end
end
end
filtered_output ||= compiled_source
rescue StandardError => e
raise "Typescript error in file '#{ts_path}':\n#{e.message}"
ensure
source_file.unlink
end
end
private
# Log a message
#
# Checks if a logger has been configured before attempting to log.
#
# @param [String] message to be logged
#
def log(message)
if Typescript::Monkey.configuration.logger
Typescript::Monkey.configuration.logger.debug(message)
end
end
# Returns module name
#
# @return [String] module name
#
def module_name
Module.nesting.last
end
end
end