diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js index 03c5e738..e0129604 100644 --- a/lib/coffee-script/coffee-script.js +++ b/lib/coffee-script/coffee-script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.6.1 (function() { - var Lexer, compile, ext, fs, helpers, lexer, loadFile, parser, path, sourcemap, vm, _i, _len, _ref, + var Lexer, compile, ext, fs, generateV3SourceMapOptions, helpers, lexer, loadFile, parser, path, sourcemap, vm, _i, _len, _ref, __hasProp = {}.hasOwnProperty; fs = require('fs'); @@ -39,16 +39,39 @@ exports.helpers = helpers; + generateV3SourceMapOptions = function(options) { + var cwd, sourceRoot; + if (options == null) { + options = {}; + } + console.log("Generating v3 source map"); + cwd = options.workingDirectory; + if (!options.filename) { + return {}; + } + if (options.jsPath) { + sourceRoot = helpers.relativePath(options.jsPath, ".", cwd); + return { + sourceRoot: sourceRoot, + sourceFile: helpers.relativePath(".", options.filename, cwd), + generatedFile: helpers.baseFileName(options.jsPath) + }; + } + return { + sourceRoot: "", + sourceFile: helpers.baseFileName(options.filename), + generatedFile: helpers.baseFileName(options.filename, true) + ".js" + }; + }; + exports.compile = compile = function(code, options) { - var answer, coffeeFile, currentColumn, currentLine, err, fragment, fragments, header, js, jsFile, merge, newLines, sourceMap, _j, _len1; + var answer, currentColumn, currentLine, err, fragment, fragments, header, js, merge, newLines, sourceMap, v3Options, _j, _len1; if (options == null) { options = {}; } merge = exports.helpers.merge; try { if (options.sourceMap) { - coffeeFile = helpers.baseFileName(options.filename); - jsFile = helpers.baseFileName(options.filename, true) + ".js"; sourceMap = new sourcemap.SourceMap(); } fragments = (parser.parse(lexer.tokenize(code, options))).compileToFragments(options); @@ -89,7 +112,8 @@ }; if (sourceMap) { answer.sourceMap = sourceMap; - answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, coffeeFile, jsFile); + v3Options = generateV3SourceMapOptions(options); + answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, v3Options); } return answer; } else { diff --git a/lib/coffee-script/command.js b/lib/coffee-script/command.js index 809dffba..bffb1e18 100644 --- a/lib/coffee-script/command.js +++ b/lib/coffee-script/command.js @@ -149,8 +149,11 @@ compileScript = function(file, input, base) { var compiled, err, o, options, t, task; + if (base == null) { + base = null; + } o = opts; - options = compileOptions(file); + options = compileOptions(file, base); try { t = task = { file: file, @@ -181,7 +184,7 @@ if (o.print) { return printLine(t.output.trim()); } else if (o.compile || o.map) { - return writeJs(base, t.file, t.output, t.sourceMap); + return writeJs(base, t.file, t.output, options.jsPath, t.sourceMap); } else if (o.lint) { return lint(t.file, t.output); } @@ -388,12 +391,11 @@ return path.join(dir, basename + extension); }; - writeJs = function(base, sourcePath, js, generatedSourceMap) { - var compile, jsDir, jsPath, sourceMapPath; + writeJs = function(base, sourcePath, js, jsPath, generatedSourceMap) { + var compile, jsDir, sourceMapPath; if (generatedSourceMap == null) { generatedSourceMap = null; } - jsPath = outputPath(sourcePath, base); sourceMapPath = outputPath(sourcePath, base, ".map"); jsDir = path.dirname(jsPath); compile = function() { @@ -480,13 +482,15 @@ } }; - compileOptions = function(filename) { + compileOptions = function(filename, base) { return { filename: filename, literate: helpers.isLiterate(filename), bare: opts.bare, header: opts.compile, - sourceMap: opts.map + sourceMap: opts.map, + jsPath: filename !== null && base !== null ? outputPath(filename, base) : null, + workingDirectory: process.cwd() }; }; diff --git a/lib/coffee-script/helpers.js b/lib/coffee-script/helpers.js index 601aa7fc..bfa7e277 100644 --- a/lib/coffee-script/helpers.js +++ b/lib/coffee-script/helpers.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.6.1 (function() { - var buildLocationData, extend, flatten, _ref; + var buildLocationData, extend, flatten, last, normalizePath, repeat, _ref; exports.starts = function(string, literal, start) { return literal === string.substr(start, literal.length); @@ -12,6 +12,19 @@ return literal === string.substr(string.length - len - (back || 0), len); }; + exports.repeat = repeat = function(str, n) { + var res; + res = ''; + while (n > 0) { + if (n & 1) { + res += str; + } + n >>>= 1; + str += str; + } + return res; + }; + exports.compact = function(array) { var item, _i, _len, _results; _results = []; @@ -70,7 +83,7 @@ return val; }; - exports.last = function(array, back) { + exports.last = last = function(array, back) { return array[array.length - (back || 0) - 1]; }; @@ -166,4 +179,67 @@ return /\.(litcoffee|coffee\.md)$/.test(file); }; + exports.normalizePath = normalizePath = function(path, removeTrailingSlash) { + var i, newParts, part, parts, root, _i, _len; + if (removeTrailingSlash == null) { + removeTrailingSlash = false; + } + root = false; + parts = path.split('/'); + newParts = []; + i = 0; + if (parts.length > 1 && parts[0] === '') { + parts.shift(); + root = true; + } + for (i = _i = 0, _len = parts.length; _i < _len; i = ++_i) { + part = parts[i]; + if (part === '.' || part === '') { + if ((i === parts.length - 1) && !removeTrailingSlash) { + newParts.push(''); + } + } else if (part === '..') { + if (newParts.length === 0 || (newParts.length && last(newParts === '..'))) { + newParts.push('..'); + } else { + newParts.pop(); + } + } else { + newParts.push(part); + } + } + if (root) { + if (newParts.length === 0) { + return '/'; + } + if (newParts.length[0] === '..') { + throw new Error("Invalid path: " + path); + } + newParts.unshift(''); + } + return newParts.join('/'); + }; + + exports.relativePath = function(from, to, cwd) { + var answer; + if (cwd == null) { + cwd = null; + } + if (cwd) { + from = cwd + "/" + from; + to = cwd + "/" + to; + } + from = normalizePath(from).split('/'); + to = normalizePath(to).split('/'); + while (from.length > 0 && to.length > 0 && from[0] === to[0]) { + from.shift(); + to.shift(); + } + if (from.length && from[0] === "..") { + throw new Error("'cwd' must be specified if 'from' references parent directory: " + (from.join('/')) + " -> " + (to.join('/'))); + } + answer = repeat("../", from.length - 1); + return answer + ("" + (to.join('/'))); + }; + }).call(this); diff --git a/lib/coffee-script/sourcemap.js b/lib/coffee-script/sourcemap.js index 245da945..34b18d15 100644 --- a/lib/coffee-script/sourcemap.js +++ b/lib/coffee-script/sourcemap.js @@ -113,14 +113,14 @@ })(); - exports.generateV3SourceMap = function(sourceMap, sourceFile, generatedFile) { - var answer, lastGeneratedColumnWritten, lastSourceColumnWritten, lastSourceLineWritten, mappings, needComma, writingGeneratedLine; - if (sourceFile == null) { - sourceFile = null; - } - if (generatedFile == null) { - generatedFile = null; + exports.generateV3SourceMap = function(sourceMap, options) { + var answer, generatedFile, lastGeneratedColumnWritten, lastSourceColumnWritten, lastSourceLineWritten, mappings, needComma, sourceFile, sourceRoot, writingGeneratedLine; + if (options == null) { + options = {}; } + sourceRoot = options.sourceRoot || ""; + sourceFile = options.sourceFile || null; + generatedFile = options.generatedFile || null; writingGeneratedLine = 0; lastGeneratedColumnWritten = 0; lastSourceLineWritten = 0; @@ -150,7 +150,7 @@ answer = { version: 3, file: generatedFile, - sourceRoot: "", + sourceRoot: sourceRoot, sources: sourceFile ? [sourceFile] : [], names: [], mappings: mappings diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index bf9a5edd..57f69e00 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -30,9 +30,41 @@ exports.VERSION = '1.6.1' # Expose helpers for testing. exports.helpers = helpers +# Generate v3 Source Map options from compile options. +# +# options.filename is required, and is the path and filename of the file being compiled, +# relative to the current working directory. +# +# `options.jsPath` and `options.workingDirectory` may also be specified to customize the output +# in the resulting v3 source map, where `options.jsPath` is the path where the .js file will be +# written relative to the current working directory, and `options.workingDirectory` is the absolute +# path of the current working directory (required if jsPath references a parent directory.) If +# these options are provided, then "sourceRoot" in the output will be a relative path to the +# current working directory, and source files will be given relative to the "sourceRoot". +# +generateV3SourceMapOptions = (options = {}) -> + console.log "Generating v3 source map" + cwd = options.workingDirectory + return {} unless options.filename + if options.jsPath + sourceRoot = helpers.relativePath options.jsPath, ".", cwd + return { + sourceRoot + sourceFile: helpers.relativePath ".", options.filename, cwd + generatedFile: helpers.baseFileName(options.jsPath) + } + { + sourceRoot: "" + sourceFile: helpers.baseFileName options.filename + generatedFile: helpers.baseFileName(options.filename, yes) + ".js" + } + + # Compile CoffeeScript code to JavaScript, using the Coffee/Jison compiler. # -# If `options.sourceMap` is specified, then `options.filename` must also be specified. +# If `options.sourceMap` is specified, then `options.filename` must also be specified. See +# `generateV3SourceMapOptions()` for other options that can be passed to control source map +# generation. # # This returns a javascript string, unless `options.sourceMap` is passed, # in which case this returns a `{js, v3SourceMap, sourceMap} @@ -43,8 +75,6 @@ exports.compile = compile = (code, options = {}) -> try if options.sourceMap - coffeeFile = helpers.baseFileName options.filename - jsFile = helpers.baseFileName(options.filename, yes) + ".js" sourceMap = new sourcemap.SourceMap() fragments = (parser.parse lexer.tokenize(code, options)).compileToFragments options @@ -80,7 +110,8 @@ exports.compile = compile = (code, options = {}) -> answer = {js} if sourceMap answer.sourceMap = sourceMap - answer.v3SourceMap = sourcemap.generateV3SourceMap sourceMap, coffeeFile, jsFile + v3Options = generateV3SourceMapOptions options + answer.v3SourceMap = sourcemap.generateV3SourceMap(sourceMap, v3Options) answer else js diff --git a/src/command.coffee b/src/command.coffee index e33dcbb4..48860469 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -112,9 +112,9 @@ compilePath = (source, topLevel, base) -> # Compile a single source script, containing the given code, according to the # requested options. If evaluating the script directly sets `__filename`, # `__dirname` and `module.filename` to be correct relative to the script's path. -compileScript = (file, input, base) -> +compileScript = (file, input, base=null) -> o = opts - options = compileOptions file + options = compileOptions file, base try t = task = {file, input, options} CoffeeScript.emit 'compile', task @@ -136,7 +136,7 @@ compileScript = (file, input, base) -> if o.print printLine t.output.trim() else if o.compile || o.map - writeJs base, t.file, t.output, t.sourceMap + writeJs base, t.file, t.output, options.jsPath, t.sourceMap else if o.lint lint t.file, t.output catch err @@ -264,8 +264,7 @@ outputPath = (source, base, extension=".js") -> # # If `generatedSourceMap` is provided, this will write a `.map` file into the # same directory as the `.js` file. -writeJs = (base, sourcePath, js, generatedSourceMap = null) -> - jsPath = outputPath sourcePath, base +writeJs = (base, sourcePath, js, jsPath, generatedSourceMap = null) -> sourceMapPath = outputPath sourcePath, base, ".map" jsDir = path.dirname jsPath compile = -> @@ -323,15 +322,19 @@ parseOptions = -> return # The compile-time options to pass to the CoffeeScript compiler. -compileOptions = (filename) -> +compileOptions = (filename, base) -> { filename literate: helpers.isLiterate(filename) bare: opts.bare header: opts.compile sourceMap: opts.map + jsPath: if (filename isnt null and base isnt null) then (outputPath filename, base) else null + workingDirectory: process.cwd() } + + # Start up a new Node.js instance with the arguments in `--nodejs` passed to # the `node` binary, preserving the other options. forkNode = -> diff --git a/src/helpers.coffee b/src/helpers.coffee index 6e700d7b..03bde1ba 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -11,6 +11,16 @@ exports.ends = (string, literal, back) -> len = literal.length literal is string.substr string.length - len - (back or 0), len +# Repeat a string `n` times. +exports.repeat = repeat = (str, n) -> + # Use clever algorithm to have O(log(n)) string concatenation operations. + res = '' + while n > 0 + res += str if n & 1 + n >>>= 1 + str += str + res + # Trim out all falsy values from an array. exports.compact = (array) -> item for item in array when item @@ -53,7 +63,7 @@ exports.del = (obj, key) -> val # Gets the last item of an array(-like) object. -exports.last = (array, back) -> array[array.length - (back or 0) - 1] +exports.last = last = (array, back) -> array[array.length - (back or 0) - 1] # Typical Array::some exports.some = Array::some ? (fn) -> @@ -120,3 +130,57 @@ exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file +# Remove any "." components in a path, any ".."s in the middle of a path. Leaves a trailing '/' +# if present, unless removeTrailingSlash is set. +exports.normalizePath = normalizePath = (path, removeTrailingSlash=no) -> + root = no # Does this path start with the root? + parts = path.split '/' + newParts = [] + i = 0 + # If the path started with a '/', set the root flag. + if parts.length > 1 and parts[0] == '' + parts.shift() + root = yes + for part, i in parts + if part in ['.', ''] + if (i is parts.length - 1) and not removeTrailingSlash + # Leave the trailing '/'. Note that we're pushing a '', but because we join with '/'s + # later, this will become a '/'. + newParts.push '' + else if part is '..' + if newParts.length is 0 or (newParts.length and last newParts is '..') + # Leave the ".." + newParts.push '..' + else + # Drop the '..' and remote the previous element + newParts.pop() + else + newParts.push part + if root + if newParts.length is 0 then return '/' + if newParts.length[0] is '..' + # Uhh... This doesn't make any sense. + throw new Error "Invalid path: #{path}" + newParts.unshift '' # Add back the leading "/" + newParts.join '/' + +# Solve the relative path from `from` to `to`. +# +# This is the same as node's `path.relative()`, but can be used even if we're not running in node. +# If paths are relative (don't have a leading '/') then we assume they are both relative to to +# same working directory. +# +# If `from` is a relative path that starts with '..', then `cwd` must be provided to resolve +# parent path names. +exports.relativePath = (from, to, cwd=null) -> + if cwd + from = cwd + "/" + from + to = cwd + "/" + to + from = normalizePath(from).split '/' + to = normalizePath(to).split '/' + while from.length > 0 and to.length > 0 and from[0] == to[0] + from.shift() + to.shift() + if from.length and from[0] is ".." then throw new Error "'cwd' must be specified if 'from' references parent directory: #{from.join '/'} -> #{to.join '/'}" + answer = repeat "../", from.length - 1 + answer + "#{to.join '/'}" diff --git a/src/sourcemap.coffee b/src/sourcemap.coffee index 52614b26..bf0d3bbf 100644 --- a/src/sourcemap.coffee +++ b/src/sourcemap.coffee @@ -92,7 +92,11 @@ class exports.SourceMap # Builds a V3 source map from a SourceMap object. # Returns the generated JSON as a string. -exports.generateV3SourceMap = (sourceMap, sourceFile=null, generatedFile=null) -> +exports.generateV3SourceMap = (sourceMap, options={}) -> + sourceRoot = options.sourceRoot or "" + sourceFile = options.sourceFile or null + generatedFile = options.generatedFile or null + writingGeneratedLine = 0 lastGeneratedColumnWritten = 0 lastSourceLineWritten = 0 @@ -146,7 +150,7 @@ exports.generateV3SourceMap = (sourceMap, sourceFile=null, generatedFile=null) - answer = { version: 3 file: generatedFile - sourceRoot: "" + sourceRoot sources: if sourceFile then [sourceFile] else [] names: [] mappings diff --git a/test/helpers.coffee b/test/helpers.coffee index 1644c565..079300eb 100644 --- a/test/helpers.coffee +++ b/test/helpers.coffee @@ -2,7 +2,7 @@ # ------- # pull the helpers from `CoffeeScript.helpers` into local variables -{starts, ends, compact, count, merge, extend, flatten, del, last, baseFileName} = CoffeeScript.helpers +{starts, ends, compact, count, merge, extend, flatten, del, last, baseFileName, normalizePath, relativePath} = CoffeeScript.helpers # `starts` @@ -126,3 +126,48 @@ test "the `baseFileName` helper returns the file name to write to", -> filename = name + ext eq filename, expectedFileName + +# `normalizePath` + +test "various tests for normalizePath", -> + eq "/", normalizePath "/" + eq "", normalizePath "." + eq "", normalizePath "" + eq "/a/b", normalizePath "/a/b" + eq "/a/c/", normalizePath "/a/c/" + eq "/a/c", normalizePath "/a/c/", true + eq "/a/d", normalizePath "/a/../a/./d/c/.." + eq "/a/e/", normalizePath "/a/../a/./e/c/../" + eq "/a/e", normalizePath "/a/../a/./e/c/../", true + eq "../a", normalizePath "../a" + eq "../b", normalizePath "a/../../b" + +# `relativePath` + +test "various tests for relativePath", -> + # Same level + eq "foo.js", relativePath "foo.coffee", "foo.js" + eq "foo.js", relativePath "foo.coffee", "foo.js", "/work/src" + # Same level, but both down one level + eq "bar.js", relativePath "src/bar.coffee", "src/bar.js" + eq "bar.js", relativePath "src/bar.coffee", "src/bar.js", "/work/src" + # Sam level, using '.'' as from + eq "baz.js", relativePath ".", "baz.js" + eq "baz.js", relativePath ".", "baz.js", "/work/src" + eq "o/qux.js", relativePath ".", "o/qux.js" + eq "o/qux.js", relativePath ".", "o/qux.js", "/work/src" + # Up one level + eq "../", relativePath "src/bar.js", "." + eq "../", relativePath "src/bar.js", ".", "/work/src" + # Up and over one directory + eq "../dest/foo.js", relativePath "src/foo.coffee", "dest/foo.js" + eq "../dest/foo.js", relativePath "src/foo.coffee", "dest/foo.js", "/work/src" + # Absolute paths + eq "dest1/dest2/bar.js", relativePath "/bar.coffee", "/dest1/dest2/bar.js" + # File vs. directory - keep trailing '/' + eq "../c", relativePath "a/b/", "a/c" + eq "../d/", relativePath "a/b/", "a/d/" + # This should throw, since relativePath can't know the name of the directory that foo.coffee is in. + throws -> relativePath "../o/foo.js", "foo.coffee" + # With the CWD, this should pass. + eq "../src/foo.coffee", relativePath "../o/foo.js", "foo.coffee", "/work/src" diff --git a/test/sourcemap.coffee b/test/sourcemap.coffee index 2770fb6c..4eede954 100644 --- a/test/sourcemap.coffee +++ b/test/sourcemap.coffee @@ -36,7 +36,11 @@ test "SourceMap tests", -> map.addMapping [1, 9], [2, 8] map.addMapping [3, 0], [3, 4] - eqJson (sourcemap.generateV3SourceMap map, "source.coffee", "source.js"), '{"version":3,"file":"source.js","sourceRoot":"","sources":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' + testWithFilenames = sourcemap.generateV3SourceMap map, { + sourceRoot: "", + sourceFile: "source.coffee", + generatedFile: "source.js"} + eqJson testWithFilenames, '{"version":3,"file":"source.js","sourceRoot":"","sources":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' eqJson (sourcemap.generateV3SourceMap map), '{"version":3,"file":null,"sourceRoot":"","sources":[],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}' # Look up a generated column - should get back the original source position.