Merge pull request #2 from weexteam/sourcemap
supported sourcemap
diff --git a/src/index.js b/src/index.js
index fbec04e..b3d4145 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,12 +4,14 @@
parseScript,
parseStyle,
parseTemplate,
- parseWeexFile
+ parseWeex
} from './parser'
+import { getFilenameByPath } from './util'
import * as config from './config'
import * as legacy from './legacy'
+import { ScriptMap } from './map'
-function partedLoader (type, loader, params, source) {
+function partedLoader (type, loader, params, source, map) {
let promise
switch (type) {
case 'js':
@@ -29,7 +31,8 @@
break
case 'we':
default:
- promise = parseWeexFile(loader, params, source)
+ map.enable()
+ promise = parseWeex(loader, params, source, map)
break
}
return promise
@@ -45,15 +48,23 @@
resourcePath: this.resourcePath
}
const type = params.loaderQuery.type || 'we'
- const promise = partedLoader(type, this, params, source)
+ const { resourcePath } = params
+ const filename = getFilenameByPath(resourcePath)
+ const map = new ScriptMap(filename, source)
+
+ const promise = partedLoader(type, this, params, source, map)
promise.then(result => {
+ if (map.enabled) {
+ map.parse()
+ }
if (type === 'style' || type === 'css' ||
type === 'html' || type === 'tpl' || type === 'template') {
result = 'module.exports=' + result
}
- callback(null, result)
+ callback(null, result, map.toJSON())
}).catch(err => {
+ // console.error(err.stack)
this.emitError(err.toString())
callback(err.toString(), '')
})
diff --git a/src/map.js b/src/map.js
new file mode 100644
index 0000000..b859690
--- /dev/null
+++ b/src/map.js
@@ -0,0 +1,109 @@
+import { SourceMapGenerator } from 'source-map'
+
+export class ScriptMap {
+ constructor (filename, content) {
+ this.filename = filename
+ this.content = content
+ const generator = new SourceMapGenerator()
+ generator.setSourceContent(filename, content)
+ this.generator = generator
+ this.history = []
+ this.elements = {}
+ this.enabled = false
+ }
+
+ enable () {
+ this.enabled = true
+ }
+
+ start () {
+ if (!this.enabled) { return }
+ this.current = { elements: [], scripts: [] }
+ }
+
+ end () {
+ if (!this.enabled) { return }
+ const current = this.current
+ this.current = {}
+
+ // re-order the elements and scripts into history
+ const length = current.elements.length
+ if (length > 0) {
+ const children = this.history.splice(-length, length)
+ current.children = children
+ }
+
+ current.elements.concat(current.scripts).forEach(item => {
+ current.name = item.name
+ delete item.name
+ })
+
+ current.elements.forEach((info, index) => {
+ current.children[index].length = info.length
+ current.children[index].line = info.line
+ })
+
+ delete current.elements
+ this.history.push(current)
+ }
+
+ addElement (name, index, line, length) {
+ if (!this.enabled) { return }
+ this.current.elements.push({ name, index, line, length })
+ }
+
+ addScript (name, info, externalOffset) {
+ if (!this.enabled) { return }
+ this.current.scripts.push({ name, info, externalOffset })
+ }
+
+ setElementPosition (name, line, column) {
+ if (!this.enabled) { return }
+ this.elements[name] = { line, column }
+ }
+
+ parse (target, startLine) {
+ if (!this.enabled) { return }
+ target = target || this.history[0]
+ if (!target) { return }
+ startLine = startLine || 0
+
+ const { name, line, scripts, children } = target
+ const elInfo = this.elements[name] || {};
+
+ (scripts || []).forEach(script => {
+ const { info, externalOffset } = script
+ const { original, generated } = info
+ const scriptLength = info.length
+ this.add(
+ original.line + (elInfo.line || 1) - 1,
+ scriptLength,
+ generated.line + startLine + (line || 1) - 1 + externalOffset
+ )
+ });
+
+ (children || []).forEach(child => {
+ this.parse(child, startLine + (line || 1) - 1)
+ })
+
+ this.json = true
+ }
+
+ add (originalLine, length, generatedLine) {
+ if (!this.enabled) { return }
+ const option = {
+ source: this.filename,
+ original: { line: originalLine, column: 1 },
+ generated: { line: generatedLine, column: 1 }
+ }
+ for (let i = 0; i < length; i++) {
+ option.original.line = originalLine + i
+ option.generated.line = generatedLine + i
+ this.generator.addMapping(option)
+ }
+ }
+
+ toJSON () {
+ return this.json ? this.generator.toJSON() : null
+ }
+}
diff --git a/src/parser.js b/src/parser.js
index ac1dd76..03b9a80 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -18,14 +18,14 @@
appendToWarn
} from './util'
-export function parseWeexFile (loader, params, source, deps, elementName) {
+export function parseWeex (loader, params, source, map, deps, elementName) {
return new Promise(
// separate source into <element>s, <template>, <style>s and <script>s
separateBlocks(source, deps || []))
// pre-parse non-javascript parts
- .then(preParseBlocks(loader, params, elementName))
+ .then(preParseBlocks(loader, params, map))
// join blocks together and parse as javascript finally
- .then(parseBlocks(loader, params, elementName))
+ .then(parseBlocks(loader, params, map, elementName))
}
function separateBlocks (source, deps) {
@@ -42,7 +42,7 @@
}
}
-function preParseBlocks (loader, params) {
+function preParseBlocks (loader, params, map) {
return (blocks) => {
const { deps, elements, template, styles, scripts, config, data } = blocks
const promises = [
@@ -60,7 +60,9 @@
const elPromises = []
Object.keys(elements).forEach(key => {
const el = elements[key]
- elPromises.push(parseWeexFile(loader, params, el.content, deps, el.name))
+ // record original positions of each <element>
+ map.setElementPosition(el.name, el.line, el.column)
+ elPromises.push(parseWeex(loader, params, el.content, map, deps, el.name))
})
promises[0] = Promise.all(elPromises)
}
@@ -80,7 +82,7 @@
}
}
-function parseBlocks (loader, params, elementName) {
+function parseBlocks (loader, params, map, elementName) {
return (results) => {
const elements = results[0] || []
const template = results[1]
@@ -94,9 +96,26 @@
let config = {}
let data
+ const mapOffset = { basic: 0, subs: [] }
+
if (scripts) {
- content += scripts.reduce((pre, cur) => {
- return pre + '\n;' + cur.content
+ // record original and generated position of each <script>
+ // the generated content is begin with empty string
+ // so later the template, styles and elements will be appended/prepended
+ // and mapOffset.basic will record lines of prepended *required* content
+ content += scripts.reduce((prev, next, i) => {
+ // length of previous content
+ const line = prev.split(/\r?\n/g).length + 1
+ const column = 1
+ const oriLine = next.line
+ const oriColumn = next.column
+ mapOffset.subs.push({
+ original: { line: oriLine, column: oriColumn },
+ generated: { line, column },
+ // length of next content
+ length: next.content.split(/\r?\n/g).length
+ })
+ return prev + '\n;' + next.content
}, '')
}
@@ -105,31 +124,39 @@
requireContent += deps.map(dep =>
depHasRequired(content, dep) ? 'require("' + dep + '");' : ''
).join('\n')
- content = requireContent + '\n' + content
+ if (requireContent) {
+ // length of implicitly requires
+ mapOffset.basic = requireContent.split(/\r?\n/g).length
+ content = requireContent + '\n' + content
+ }
}
if (template) {
+ // append template content, not impact sourcemap
content += '\n;module.exports.template = module.exports.template || {}' +
'\n;Object.assign(module.exports.template, ' + template + ')'
}
if (style) {
+ // append style content, not impact sourcemap
content += '\n;module.exports.style = module.exports.style || {}' +
'\n;Object.assign(module.exports.style, ' + style + ')'
}
+ // prepare entry config
if (configResult) {
config = new Function('return ' + configResult.content.replace(/\n/g, ''))()
}
config.transformerVersion = transformerVersion
config = JSON.stringify(config, null, 2)
+ // prepare entry data
if (dataResult) {
data = new Function('return ' + dataResult.content.replace(/\n/g, ''))()
data = JSON.stringify(data, null, 2)
}
- return parseScript(loader, params, content, { config, data, elementName, elements })
+ return parseScript(loader, params, content, { config, data, elementName, elements, map, mapOffset })
}
}
@@ -175,7 +202,7 @@
}
export function parseScript (loader, params, source, env) {
- const { config, data, elementName, elements } = env
+ const { config, data, elementName, elements, map, mapOffset } = env
// the entry component has a special resource query and not a sub element tag
const isEntry = params.resourceQuery.entry === true && !elementName
@@ -185,10 +212,22 @@
? md5(source)
: (elementName || params.resourceQuery.name || getNameByPath(params.resourcePath))
+ // join with elements deps
+ // 2 more lines between each element and the end
+ map && map.start()
+ const prefix = (elements || []).reduce((prev, next, index) => {
+ const prevLength = prev.split(/\r?\n/g).length
+ const nextLength = next.split(/\r?\n/g).length
+ // record generated positions of each <element>
+ map && map.addElement(name, index, prevLength, nextLength)
+ return prev + next + ';\n\n'
+ }, '')
+
// fix data option from an object to a function
let target = scripter.fix(source)
// wrap with __weex_define__(name, [], (r, e, m) {...})
+ // 1 more line at start, 1 more line at end
target = target
.replace(MODULE_EXPORTS_REG, '__weex_module__.exports')
.replace(REQUIRE_REG, '__weex_require__($1$2$1)')
@@ -196,16 +235,24 @@
'function(__weex_require__, __weex_exports__, __weex_module__)' +
'{\n' + target + '\n})'
+ // record mapOffset into sourcemap
+ if (mapOffset) {
+ // length of generated prefix (elements) and basic (implicitly requires)
+ const preLines = prefix.split(/\r?\n/g).length + mapOffset.basic
+ mapOffset.subs.forEach(info => {
+ map.addScript(elementName || name, info, preLines)
+ })
+ }
+ map && map.end()
+
// append __weex_bootstrap__ for entry component
+ // not impact sourcemap
if (isEntry) {
target += '\n;__weex_bootstrap__("@weex-component/' + name + '", ' +
String(config) + ',' +
String(data) + ')'
}
- // join with elements deps
- target = (elements || []).concat(target).join(';\n\n')
-
- return Promise.resolve(target)
+ return Promise.resolve(prefix + target)
}
diff --git a/src/util.js b/src/util.js
index 3417388..d631779 100644
--- a/src/util.js
+++ b/src/util.js
@@ -10,6 +10,10 @@
return path.basename(filepath).replace(/\..*$/, '')
}
+export function getFilenameByPath (filepath) {
+ return path.relative('.', filepath)
+}
+
export const FUNC_START = '#####FUN_S#####'
export const FUNC_START_REG = new RegExp('["\']' + FUNC_START, 'g')
export const FUNC_END = '#####FUN_E#####'
diff --git a/test/a.js b/test/a.js
index b8f9f49..574e715 100644
--- a/test/a.js
+++ b/test/a.js
@@ -10,4 +10,4 @@
}
module.exports.style = require('./a.less');
-module.exports.template = require('./a.tpl');
\ No newline at end of file
+module.exports.template = require('./a.tpl');
diff --git a/test/expect/a.we b/test/expect/a.we
index 2ea3b36..2565076 100644
--- a/test/expect/a.we
+++ b/test/expect/a.we
@@ -24,4 +24,4 @@
text: 'Hello ' + c.name
}
}
-</script>
\ No newline at end of file
+</script>
diff --git a/test/expect/sourcemap.we b/test/expect/sourcemap.we
new file mode 100644
index 0000000..f798f7f
--- /dev/null
+++ b/test/expect/sourcemap.we
@@ -0,0 +1,26 @@
+<element name="foo">
+ <template>
+ <text>Hello</text>
+ </template>
+ <script>
+ console.log(6)
+ console.log(7)
+ console.log(8)
+ console.log(9)
+ console.log(0)
+ </script>
+</element>
+
+<template>
+ <div>
+ <foo></foo>
+ </div>
+</template>
+
+<script>
+ console.log(1)
+ console.log(2)
+ require('../lib/3rd.js')
+ console.log(4)
+ console.log(5)
+</script>
diff --git a/test/lib/3rd.js b/test/lib/3rd.js
index e132ec8..cc00e41 100644
--- a/test/lib/3rd.js
+++ b/test/lib/3rd.js
@@ -1 +1,4 @@
'Hello 3rd-party JavaScript'
+'Hello 3rd-party JavaScript 2'
+'Hello 3rd-party JavaScript 3'
+'Hello 3rd-party JavaScript 4'
diff --git a/test/test.js b/test/test.js
index ab14972..e2a616d 100644
--- a/test/test.js
+++ b/test/test.js
@@ -7,6 +7,9 @@
var expect = chai.expect;
chai.use(sinonChai);
+var webpack = require('webpack')
+var SourceMapConsumer = require('source-map').SourceMapConsumer
+
require('./lib/jsfm');
var createInstance = global.createInstance;
var getRoot = global.getRoot;
@@ -25,10 +28,10 @@
var name = 'a.js';
var actualCodePath = path.resolve(__dirname, 'actual', name);
- var actualCodeContent = fs.readFileSync(actualCodePath);
+ var actualCodeContent = fs.readFileSync(actualCodePath, { encoding: 'utf8' });
var expectCodePath = path.resolve(__dirname, 'expect', name);
- var expectCodeContent = fs.readFileSync(expectCodePath);
+ var expectCodeContent = fs.readFileSync(expectCodePath, { encoding: 'utf8' });
var actualResult = createInstance('actual/' + name, actualCodeContent);
@@ -44,10 +47,10 @@
var name = 'b.js';
var actualCodePath = path.resolve(__dirname, 'actual', name);
- var actualCodeContent = fs.readFileSync(actualCodePath);
+ var actualCodeContent = fs.readFileSync(actualCodePath, { encoding: 'utf8' });
var expectCodePath = path.resolve(__dirname, 'expect', name);
- var expectCodeContent = fs.readFileSync(expectCodePath);
+ var expectCodeContent = fs.readFileSync(expectCodePath, { encoding: 'utf8' });
var actualResult = createInstance('actual/' + name, actualCodeContent);
@@ -63,10 +66,10 @@
var name = 'z.js';
var actualCodePath = path.resolve(__dirname, 'actual', name);
- var actualCodeContent = fs.readFileSync(actualCodePath);
+ var actualCodeContent = fs.readFileSync(actualCodePath, { encoding: 'utf8' });
var expectCodePath = path.resolve(__dirname, 'expect', name);
- var expectCodeContent = fs.readFileSync(expectCodePath);
+ var expectCodeContent = fs.readFileSync(expectCodePath, { encoding: 'utf8' });
var actualResult = createInstance('actual/' + name, actualCodeContent);
@@ -77,4 +80,53 @@
expect(actualJson).eql(expectJson);
});
+
+ it('support source map', function() {
+ var name = 'sourcemap'
+
+ var mapPath = path.resolve(__dirname, 'actual', name + '.js.map');
+ var map = fs.readFileSync(mapPath, { encoding: 'utf8' });
+ var smc = new SourceMapConsumer(JSON.parse(map))
+
+ var oriPath = path.resolve(__dirname, 'expect', name + '.we');
+ var ori = fs.readFileSync(oriPath, { encoding: 'utf8' });
+
+ var genPath = path.resolve(__dirname, 'actual', name + '.js');
+ var gen = fs.readFileSync(genPath, { encoding: 'utf8' });
+
+ function matchPos(code, regexp) {
+ var line, col
+ code.split(/\r?\n/g).some(function (l, i) {
+ if (regexp.test(l)) {
+ line = i + 1
+ col = l.length
+ return true
+ }
+ })
+ return { line: line, col: col }
+ }
+
+ function checkPos(regexp) {
+ var genPos = matchPos(gen, regexp)
+ var oriPos = matchPos(ori, regexp)
+
+ var pos = smc.originalPositionFor({
+ line: genPos.line,
+ column: genPos.col
+ })
+
+ expect(pos.source.indexOf('sourcemap.we') > -1)
+ expect(pos.line).to.equal(oriPos.line)
+ }
+
+ checkPos(/console\.log\(1\)/)
+ checkPos(/console\.log\(2\)/)
+ checkPos(/console\.log\(4\)/)
+ checkPos(/console\.log\(5\)/)
+ checkPos(/console\.log\(6\)/)
+ checkPos(/console\.log\(7\)/)
+ checkPos(/console\.log\(8\)/)
+ checkPos(/console\.log\(9\)/)
+ checkPos(/console\.log\(0\)/)
+ })
})
diff --git a/test/webpack.config.js b/test/webpack.config.js
index 4833d07..68792f3 100644
--- a/test/webpack.config.js
+++ b/test/webpack.config.js
@@ -2,6 +2,7 @@
module.exports = {
entry: {
+ sourcemap: path.resolve(__dirname, 'expect/sourcemap.we?entry=true'),
a: path.resolve(__dirname, 'a.js?entry=true'),
b: path.resolve(__dirname, 'expect/b.we?entry=true'),
z: path.resolve(__dirname, 'expect/z.we?entry=true')
@@ -10,6 +11,7 @@
path: path.resolve(__dirname, 'actual'),
filename: '[name].js'
},
+ devtool: 'source-map',
module: {
loaders: [
{