From d6e1a979e4d6c2461290bb480c66cc385126df0f Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Wed, 6 Mar 2013 11:05:57 -0500 Subject: [PATCH 1/3] Fix sourceRoot and relative path for .coffee files in generated source maps. --- lib/coffee-script/coffee-script.js | 41 ++++++++++++++++++++++++++---- lib/coffee-script/command.js | 17 ++++++++----- lib/coffee-script/sourcemap.js | 16 ++++++------ src/coffee-script.coffee | 29 ++++++++++++++++++--- src/command.coffee | 14 +++++----- src/sourcemap.coffee | 8 ++++-- test/sourcemap.coffee | 6 ++++- 7 files changed, 99 insertions(+), 32 deletions(-) diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js index 03c5e738..ab4b0115 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,46 @@ exports.helpers = helpers; + generateV3SourceMapOptions = function(options) { + var pathDepth, sourceRoot, _j; + if (options == null) { + options = {}; + } + if (!options.filename) { + return {}; + } else { + if (options.jsPath) { + pathDepth = options.jsPath.split('/').length - 1; + sourceRoot = ""; + if (pathDepth > 0) { + for (_j = 0; 0 <= pathDepth ? _j < pathDepth : _j > pathDepth; 0 <= pathDepth ? _j++ : _j--) { + sourceRoot += "../"; + } + } + return { + sourceRoot: sourceRoot, + sourceFile: options.filename, + generatedFile: helpers.baseFileName(options.jsPath) + }; + } else { + 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 +119,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..39295930 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,14 @@ } }; - 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 }; }; 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..8f837f64 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -30,6 +30,30 @@ exports.VERSION = '1.6.1' # Expose helpers for testing. exports.helpers = helpers +generateV3SourceMapOptions = (options = {}) -> + if !options.filename + return {} + else + if options.jsPath + # jsPath is relative to the CWD. Construct a sourceRoot that gets us back to the CWD. + pathDepth = options.jsPath.split('/').length-1 + sourceRoot = "" + if (pathDepth > 0) then for [0...pathDepth] + sourceRoot += "../" + + return { + sourceRoot, + sourceFile: options.filename, + generatedFile: helpers.baseFileName(options.jsPath) + } + else + return { + 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. @@ -43,8 +67,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 +102,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..f1fd23fa 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,18 @@ 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 } + + # 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/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/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. From ce6772f2be0f8513f54fc90d56bb6f58e73ef9f6 Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Wed, 6 Mar 2013 15:45:47 -0500 Subject: [PATCH 2/3] Better fix for sourceRoot and relative path for .coffee files in source maps. --- lib/coffee-script/coffee-script.js | 39 +++++++--------- lib/coffee-script/command.js | 3 +- lib/coffee-script/helpers.js | 72 +++++++++++++++++++++++++++++- src/coffee-script.coffee | 52 ++++++++++++--------- src/command.coffee | 1 + src/helpers.coffee | 59 ++++++++++++++++++++++++ test/helpers.coffee | 47 ++++++++++++++++++- 7 files changed, 225 insertions(+), 48 deletions(-) diff --git a/lib/coffee-script/coffee-script.js b/lib/coffee-script/coffee-script.js index ab4b0115..e0129604 100644 --- a/lib/coffee-script/coffee-script.js +++ b/lib/coffee-script/coffee-script.js @@ -40,35 +40,28 @@ exports.helpers = helpers; generateV3SourceMapOptions = function(options) { - var pathDepth, sourceRoot, _j; + var cwd, sourceRoot; if (options == null) { options = {}; } + console.log("Generating v3 source map"); + cwd = options.workingDirectory; if (!options.filename) { return {}; - } else { - if (options.jsPath) { - pathDepth = options.jsPath.split('/').length - 1; - sourceRoot = ""; - if (pathDepth > 0) { - for (_j = 0; 0 <= pathDepth ? _j < pathDepth : _j > pathDepth; 0 <= pathDepth ? _j++ : _j--) { - sourceRoot += "../"; - } - } - return { - sourceRoot: sourceRoot, - sourceFile: options.filename, - generatedFile: helpers.baseFileName(options.jsPath) - }; - } else { - return { - sourceRoot: "", - sourceFile: helpers.baseFileName(options.filename, { - generatedFile: helpers.baseFileName(options.filename, true) + ".js" - }) - }; - } } + 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) { diff --git a/lib/coffee-script/command.js b/lib/coffee-script/command.js index 39295930..bffb1e18 100644 --- a/lib/coffee-script/command.js +++ b/lib/coffee-script/command.js @@ -489,7 +489,8 @@ bare: opts.bare, header: opts.compile, sourceMap: opts.map, - jsPath: filename !== null && base !== null ? outputPath(filename, base) : null + 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 aedb4bdf..c7c04813 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, normalizePath, _ref; exports.starts = function(string, literal, start) { return literal === string.substr(start, literal.length); @@ -166,4 +166,74 @@ return /\.(litcoffee|coffee\.md)$/.test(file); }; + exports.normalizePath = normalizePath = function(path, removeTrailingSlash) { + var i, parts, root, _ref1; + if (removeTrailingSlash == null) { + removeTrailingSlash = false; + } + root = false; + parts = path.split('/'); + i = 0; + if (parts.length > 1 && parts[i] === '') { + parts.splice(i, 1); + root = true; + } + while (i < parts.length) { + if ((_ref1 = parts[i]) === '.' || _ref1 === '') { + if ((i === parts.length - 1) && !removeTrailingSlash) { + parts[i] = ''; + i++; + } else { + parts.splice(i, 1); + } + } else if (parts[i] === '..') { + if (i === 0 || (i && parts[i - 1] === '..')) { + i++; + } else { + parts.splice(i - 1, 2); + i--; + } + } else { + i++; + } + } + if (root) { + if (parts.length === 0) { + return '/'; + } + if (parts.length[0] === '..') { + throw new Error("Invalid path: " + path); + } + parts.unshift(''); + } + return parts.join('/'); + }; + + exports.relativePath = function(from, to, cwd) { + var answer, _i, _ref1; + 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 = ""; + if (from.length > 1) { + for (_i = 0, _ref1 = from.length - 1; 0 <= _ref1 ? _i < _ref1 : _i > _ref1; 0 <= _ref1 ? _i++ : _i--) { + answer += "../"; + } + } + return answer + ("" + (to.join('/'))); + }; + }).call(this); diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index 8f837f64..57f69e00 100644 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -30,33 +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 = {}) -> - if !options.filename - return {} - else - if options.jsPath - # jsPath is relative to the CWD. Construct a sourceRoot that gets us back to the CWD. - pathDepth = options.jsPath.split('/').length-1 - sourceRoot = "" - if (pathDepth > 0) then for [0...pathDepth] - sourceRoot += "../" - - return { - sourceRoot, - sourceFile: options.filename, - generatedFile: helpers.baseFileName(options.jsPath) - } - else - return { - sourceRoot: "", - sourceFile: helpers.baseFileName options.filename, - generatedFile: helpers.baseFileName(options.filename, yes) + ".js" - } + 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} diff --git a/src/command.coffee b/src/command.coffee index f1fd23fa..48860469 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -330,6 +330,7 @@ compileOptions = (filename, base) -> header: opts.compile sourceMap: opts.map jsPath: if (filename isnt null and base isnt null) then (outputPath filename, base) else null + workingDirectory: process.cwd() } diff --git a/src/helpers.coffee b/src/helpers.coffee index d0f9dd1e..10d38903 100644 --- a/src/helpers.coffee +++ b/src/helpers.coffee @@ -120,3 +120,62 @@ 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 '/' + i = 0 + # If the path started with a '/', set the root flag. + if parts.length > 1 and parts[i] == '' + parts.splice i, 1 + root = yes + while i < parts.length + if parts[i] in ['.', ''] + if (i is parts.length - 1) and not removeTrailingSlash + # Leave the trailing '/'' + parts[i] = '' + i++ + else + # Remove the empty element + parts.splice i, 1 + else if parts[i] is '..' + if i is 0 or (i and parts[i-1] is '..') + # Leave the ".." + i++ + else + # Remove the '..' and the previous element + parts.splice i-1, 2 + i-- + else + i++ + if root + if parts.length == 0 then return '/' + if parts.length[0] is '..' + # Uhh... This doesn't make any sense. + throw new Error "Invalid path: #{path}" + parts.unshift '' # Add back the leading "/" + parts.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 = "" + if from.length > 1 then for [0...(from.length - 1)] + answer += "../" + answer + "#{to.join '/'}" diff --git a/test/helpers.coffee b/test/helpers.coffee index 66819084..2db4fdfb 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} = CoffeeScript.helpers +{starts, ends, compact, count, merge, extend, flatten, del, last, normalizePath, relativePath} = CoffeeScript.helpers # `starts` @@ -94,3 +94,48 @@ test "the `last` helper returns the last item of an array-like object", -> test "the `last` helper allows one to specify an optional offset", -> ary = [0, 1, 2, 3, 4] eq 2, last(ary, 2) + +# `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" From 185b2ce632f5371a8a90cf72cb1b6f05c18708ab Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Wed, 6 Mar 2013 16:26:34 -0500 Subject: [PATCH 3/3] Code inspect fixes from the inimitable Nami-Doc --- lib/coffee-script/helpers.js | 62 ++++++++++++++++++++---------------- src/helpers.coffee | 59 ++++++++++++++++++---------------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/lib/coffee-script/helpers.js b/lib/coffee-script/helpers.js index c7c04813..8f97efff 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, normalizePath, _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]; }; @@ -167,50 +180,48 @@ }; exports.normalizePath = normalizePath = function(path, removeTrailingSlash) { - var i, parts, root, _ref1; + 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[i] === '') { - parts.splice(i, 1); + if (parts.length > 1 && parts[0] === '') { + parts.shift(); root = true; } - while (i < parts.length) { - if ((_ref1 = parts[i]) === '.' || _ref1 === '') { + for (i = _i = 0, _len = parts.length; _i < _len; i = ++_i) { + part = parts[i]; + if (part === '.' || part === '') { if ((i === parts.length - 1) && !removeTrailingSlash) { - parts[i] = ''; - i++; - } else { - parts.splice(i, 1); + newParts.push(''); } - } else if (parts[i] === '..') { - if (i === 0 || (i && parts[i - 1] === '..')) { - i++; + } else if (part === '..') { + if (newParts.length === 0 || (newParts.length && last(newParts === '..'))) { + newParts.push('..'); } else { - parts.splice(i - 1, 2); - i--; + newParts.pop(); } } else { - i++; + newParts.push(part); } } if (root) { - if (parts.length === 0) { + if (newParts.length === 0) { return '/'; } - if (parts.length[0] === '..') { + if (newParts.length[0] === '..') { throw new Error("Invalid path: " + path); } - parts.unshift(''); + newParts.unshift(''); } - return parts.join('/'); + return newParts.join('/'); }; exports.relativePath = function(from, to, cwd) { - var answer, _i, _ref1; + var answer; if (cwd == null) { cwd = null; } @@ -227,12 +238,7 @@ if (from.length && from[0] === "..") { throw new Error("'cwd' must be specified if 'from' references parent directory: " + (from.join('/')) + " -> " + (to.join('/'))); } - answer = ""; - if (from.length > 1) { - for (_i = 0, _ref1 = from.length - 1; 0 <= _ref1 ? _i < _ref1 : _i > _ref1; 0 <= _ref1 ? _i++ : _i--) { - answer += "../"; - } - } + answer = repeat("../", from.length - 1); return answer + ("" + (to.join('/'))); }; diff --git a/src/helpers.coffee b/src/helpers.coffee index 10d38903..91b096de 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) -> @@ -125,37 +135,34 @@ exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file 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[i] == '' - parts.splice i, 1 + if parts.length > 1 and parts[0] == '' + parts.shift() root = yes - while i < parts.length - if parts[i] in ['.', ''] + for part, i in parts + if part in ['.', ''] if (i is parts.length - 1) and not removeTrailingSlash - # Leave the trailing '/'' - parts[i] = '' - i++ - else - # Remove the empty element - parts.splice i, 1 - else if parts[i] is '..' - if i is 0 or (i and parts[i-1] is '..') + # 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 ".." - i++ + newParts.push '..' else - # Remove the '..' and the previous element - parts.splice i-1, 2 - i-- + # Drop the '..' and remote the previous element + newParts.pop() else - i++ + newParts.push part if root - if parts.length == 0 then return '/' - if parts.length[0] is '..' + 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}" - parts.unshift '' # Add back the leading "/" - parts.join '/' + newParts.unshift '' # Add back the leading "/" + newParts.join '/' # Solve the relative path from `from` to `to`. # @@ -169,13 +176,11 @@ exports.relativePath = (from, to, cwd=null) -> if cwd from = cwd + "/" + from to = cwd + "/" + to - from = (normalizePath from).split '/' - to = (normalizePath to).split '/' + 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 = "" - if from.length > 1 then for [0...(from.length - 1)] - answer += "../" + answer = repeat "../", from.length - 1 answer + "#{to.join '/'}"