diff --git a/package.json b/package.json index dbfcea1..0ebf26c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@vue/component-compiler-utils": "^1.2.1", "clean-css": "^4.1.11", "hash-sum": "^1.0.2", - "postcss-modules-sync": "^1.0.0" + "postcss-modules-sync": "^1.0.0", + "source-map": "0.6.*" } } diff --git a/src/assembler.ts b/src/assembler.ts index 5198d6e..2c554f6 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -1,4 +1,8 @@ +import { SourceMapGenerator } from 'source-map' import { SFCCompiler, DescriptorCompileResult } from './compiler' +import { merge } from './source-map' + +// const merge = require('merge-source-map') export interface AssembleSource { filename: string @@ -35,17 +39,22 @@ export function assemble( return assembleFromSource(compiler, options, { filename, scopeId: result.scopeId, - script: result.script && { source: result.script.code }, + script: result.script && { + source: result.script.code, + map: result.script.map + }, template: result.template && { + ...result.template, source: result.template.code, functional: result.template.functional - }, + } as any, styles: result.styles.map(style => { if (style.errors.length) { console.error(style.errors) } return { + ...style, source: style.code, media: style.media, scoped: style.scoped, @@ -63,9 +72,11 @@ export function assembleFromSource( ): AssembleResults { script = script || { source: 'export default {}' } template = template || { source: '' } + let map = undefined + const mapGenerator = new SourceMapGenerator({ file: filename }) const hasScopedStyle = styles.some(style => style.scoped === true) - const hasStyle = styles.some(style => (style.source || style.module)) + const hasStyle = styles.some(style => style.source || style.module) const e = (any: any): string => JSON.stringify(any) const createImport = (name: string, value: string) => value.startsWith('~') @@ -76,279 +87,339 @@ export function assembleFromSource( // language=JavaScript const inlineCreateInjector = `function __vue_create_injector__() { - const head = document.head || document.getElementsByTagName('head')[0] - const styles = __vue_create_injector__.styles || (__vue_create_injector__.styles = {}) - const isOldIE = - typeof navigator !== 'undefined' && - /msie [6-9]\\\\b/.test(navigator.userAgent.toLowerCase()) - - return function addStyle(id, css) { - if (document.querySelector('style[data-vue-ssr-id~="' + id + '"]')) return // SSR styles are present. - - const group = isOldIE ? css.media || 'default' : id - const style = styles[group] || (styles[group] = { ids: [], parts: [], element: undefined }) - - if (!style.ids.includes(id)) { - let code = css.source - let index = style.ids.length - - style.ids.push(id) - - if (${e(compiler.template.isProduction)} && css.map) { - // https://developer.chrome.com/devtools/docs/javascript-debugging - // this makes source maps inside style tags work properly in Chrome - code += '\\n/*# sourceURL=' + css.map.sources[0] + ' */' - // http://stackoverflow.com/a/26603875 - code += - '\\n/*# sourceMappingURL=data:application/json;base64,' + - btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + - ' */' - } - - if (isOldIE) { - style.element = style.element || document.querySelector('style[data-group=' + group + ']') - } - - if (!style.element) { - const el = style.element = document.createElement('style') - el.type = 'text/css' + const head = document.head || document.getElementsByTagName('head')[0] + const styles = __vue_create_injector__.styles || (__vue_create_injector__.styles = {}) + const isOldIE = + typeof navigator !== 'undefined' && + /msie [6-9]\\\\b/.test(navigator.userAgent.toLowerCase()) + + return function addStyle(id, css) { + if (document.querySelector('style[data-vue-ssr-id~="' + id + '"]')) return // SSR styles are present. + + const group = isOldIE ? css.media || 'default' : id + const style = styles[group] || (styles[group] = { ids: [], parts: [], element: undefined }) + + if (!style.ids.includes(id)) { + let code = css.source + let index = style.ids.length + + style.ids.push(id) + + if (${e(compiler.template.isProduction)} && css.map) { + // https://developer.chrome.com/devtools/docs/javascript-debugging + // this makes source maps inside style tags work properly in Chrome + code += '\\n/*# sourceURL=' + css.map.sources[0] + ' */' + // http://stackoverflow.com/a/26603875 + code += + '\\n/*# sourceMappingURL=data:application/json;base64,' + + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + + ' */' + } - if (css.media) el.setAttribute('media', css.media) if (isOldIE) { - el.setAttribute('data-group', group) - el.setAttribute('data-next-index', '0') + style.element = style.element || document.querySelector('style[data-group=' + group + ']') } - head.appendChild(el) - } + if (!style.element) { + const el = style.element = document.createElement('style') + el.type = 'text/css' - if (isOldIE) { - index = parseInt(style.element.getAttribute('data-next-index')) - style.element.setAttribute('data-next-index', index + 1) - } + if (css.media) el.setAttribute('media', css.media) + if (isOldIE) { + el.setAttribute('data-group', group) + el.setAttribute('data-next-index', '0') + } - if (style.element.styleSheet) { - style.parts.push(code) - style.element.styleSheet.cssText = style.parts - .filter(Boolean) - .join('\\n') - } else { - const textNode = document.createTextNode(code) - const nodes = style.element.childNodes - if (nodes[index]) style.element.removeChild(nodes[index]) - if (nodes.length) style.element.insertBefore(textNode, nodes[index]) - else style.element.appendChild(textNode) + head.appendChild(el) + } + + if (isOldIE) { + index = parseInt(style.element.getAttribute('data-next-index')) + style.element.setAttribute('data-next-index', index + 1) + } + + if (style.element.styleSheet) { + style.parts.push(code) + style.element.styleSheet.cssText = style.parts + .filter(Boolean) + .join('\\n') + } else { + const textNode = document.createTextNode(code) + const nodes = style.element.childNodes + if (nodes[index]) style.element.removeChild(nodes[index]) + if (nodes.length) style.element.insertBefore(textNode, nodes[index]) + else style.element.appendChild(textNode) + } } } - } -}` + }` const createInjector = options.styleInjector ? createImport('__vue_create_injector__', options.styleInjector) : inlineCreateInjector // language=JavaScript const inlineCreateInjectorSSR = `function __vue_create_injector_ssr__(context) { - if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { - context = __VUE_SSR_CONTEXT__ - } - - if (!context) return function () {} + if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { + context = __VUE_SSR_CONTEXT__ + } - if (!context.hasOwnProperty('styles')) { - Object.defineProperty(context, 'styles', { - enumerable: true, - get: () => context._styles - }) - context._renderStyles = renderStyles - } + if (!context) return function () {} - function renderStyles(styles) { - let css = '' - for (const {ids, media, parts} of styles) { - css += - '' + if (!context.hasOwnProperty('styles')) { + Object.defineProperty(context, 'styles', { + enumerable: true, + get: () => context._styles + }) + context._renderStyles = renderStyles } - return css - } + function renderStyles(styles) { + let css = '' + for (const {ids, media, parts} of styles) { + css += + '' + } - return function addStyle(id, css) { - const group = ${e( - compiler.template.isProduction - )} ? css.media || 'default' : id - const style = context._styles[group] || (context._styles[group] = { ids: [], parts: [] }) - - if (!style.ids.includes(id)) { - style.media = css.media - style.ids.push(id) - let code = css.source - if (${e(!compiler.template.isProduction)} && css.map) { - // https://developer.chrome.com/devtools/docs/javascript-debugging - // this makes source maps inside style tags work properly in Chrome - code += '\\n/*# sourceURL=' + css.map.sources[0] + ' */' - // http://stackoverflow.com/a/26603875 - code += - '\\n/*# sourceMappingURL=data:application/json;base64,' + - btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + - ' */' + return css + } + + return function addStyle(id, css) { + const group = ${e( + compiler.template.isProduction + )} ? css.media || 'default' : id + const style = context._styles[group] || (context._styles[group] = { ids: [], parts: [] }) + + if (!style.ids.includes(id)) { + style.media = css.media + style.ids.push(id) + let code = css.source + if (${e(!compiler.template.isProduction)} && css.map) { + // https://developer.chrome.com/devtools/docs/javascript-debugging + // this makes source maps inside style tags work properly in Chrome + code += '\\n/*# sourceURL=' + css.map.sources[0] + ' */' + // http://stackoverflow.com/a/26603875 + code += + '\\n/*# sourceMappingURL=data:application/json;base64,' + + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + + ' */' + } + style.parts.push(code) } - style.parts.push(code) } - } -}` + }` const createInjectorSSR = options.styleInjectorSSR ? createImport('__vue_create_injector_ssr__', options.styleInjectorSSR) : inlineCreateInjectorSSR // language=JavaScript const inlineNormalizeComponent = `function __vue_normalize__( - template, style, script, - scope, functional, moduleIdentifier, - createInjector, createInjectorSSR -) { - const component = (typeof script === 'function' ? script.options : script) || {} - - if (${e(!compiler.template.isProduction)}) { - component.__file = ${e(filename)} - } + template, style, script, + scope, functional, moduleIdentifier, + createInjector, createInjectorSSR + ) { + const component = (typeof script === 'function' ? script.options : script) || {} + + if (${e(!compiler.template.isProduction)}) { + component.__file = ${e(filename)} + } - if (!component.render) { - component.render = template.render - component.staticRenderFns = template.staticRenderFns - component._compiled = true + if (!component.render) { + component.render = template.render + component.staticRenderFns = template.staticRenderFns + component._compiled = true - if (functional) component.functional = true - } + if (functional) component.functional = true + } - component._scopeId = scope - - if (${e(hasStyle)}) { - let hook - if (${e(compiler.template.optimizeSSR)}) { - // In SSR. - hook = function(context) { - // 2.3 injection - context = - context || // cached call - (this.$vnode && this.$vnode.ssrContext) || // stateful - (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional - // 2.2 with runInNewContext: true - if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { - context = __VUE_SSR_CONTEXT__ - } - // inject component styles - if (style) { - style.call(this, createInjectorSSR(context)) - } - // register component module identifier for async chunk inference - if (context && context._registeredComponents) { - context._registeredComponents.add(moduleIdentifier) + component._scopeId = scope + + if (${e(hasStyle)}) { + let hook + if (${e(compiler.template.optimizeSSR)}) { + // In SSR. + hook = function(context) { + // 2.3 injection + context = + context || // cached call + (this.$vnode && this.$vnode.ssrContext) || // stateful + (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional + // 2.2 with runInNewContext: true + if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { + context = __VUE_SSR_CONTEXT__ + } + // inject component styles + if (style) { + style.call(this, createInjectorSSR(context)) + } + // register component module identifier for async chunk inference + if (context && context._registeredComponents) { + context._registeredComponents.add(moduleIdentifier) + } } + // used by ssr in case component is cached and beforeCreate + // never gets called + component._ssrRegister = hook } - // used by ssr in case component is cached and beforeCreate - // never gets called - component._ssrRegister = hook - } - else if (style) { - hook = function(context) { - style.call(this, createInjector(context)) + else if (style) { + hook = function(context) { + style.call(this, createInjector(context)) + } } - } - if (hook !== undefined) { - if (component.functional) { - // register for functional component in vue file - const originalRender = component.render - component.render = function renderWithStyleInjection(h, context) { - hook.call(context) - return originalRender(h, context) + if (hook !== undefined) { + if (component.functional) { + // register for functional component in vue file + const originalRender = component.render + component.render = function renderWithStyleInjection(h, context) { + hook.call(context) + return originalRender(h, context) + } + } else { + // inject component registration as beforeCreate hook + const existing = component.beforeCreate + component.beforeCreate = existing ? [].concat(existing, hook) : [hook] } - } else { - // inject component registration as beforeCreate hook - const existing = component.beforeCreate - component.beforeCreate = existing ? [].concat(existing, hook) : [hook] } } - } - return component -}` + return component + }` const normalizeComponent = options.normalizer ? createImport('__vue_normalize__', options.normalizer) : inlineNormalizeComponent + const DEFAULT_EXPORT = 'const __vue_script__ =' // language=JavaScript - const code = ` -/* script */ -${script.source.replace(/export default/, 'const __vue_script__ =')} -/* template */ -${template.source - .replace('var render =', 'var __vue_render__ =') - .replace('var staticRenderFns =', 'var __vue_staticRenderFns__ =') - .replace('render._withStripped =', '__vue_render__._withStripped =')} -const __vue_template__ = typeof __vue_render__ !== 'undefined' - ? { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ } - : {} -/* style */ -const __vue_inject_styles__ = ${hasStyle} ? function (inject) { - if (!inject) return - ${styles.map((style, index) => { - const source = IDENTIFIER.test(style.source) - ? style.source - : e(style.source) - const map = !compiler.template.isProduction - ? typeof style.map === 'string' && IDENTIFIER.test(style.map) - ? style.map - : o(style.map) - : undefined - const tokens = - typeof style.module === 'string' && IDENTIFIER.test(style.module) - ? style.module - : o(style.module) - - return ( - (source - ? `inject("${scopeId + - '_' + - index}", { source: ${source}, map: ${map}, media: ${e( - style.media - )} })\n` - : '') + - (style.moduleName - ? `Object.defineProperty(this, "${ - style.moduleName - }", { value: ${tokens} })` + '\n' - : '') - ) - })} -} : undefined -/* scoped */ -const __vue_scope_id__ = ${e(hasScopedStyle)} ? "${scopeId}" : undefined -/* module identifier */ -const __vue_module_identifier__ = ${e( - compiler.template.optimizeSSR - )} ? "${scopeId}" : undefined -/* functional template */ -const __vue_is_functional_template__ = ${e(template.functional)} -/* component normalizer */ -${normalizeComponent} -/* style inject */ -${!compiler.template.optimizeSSR ? createInjector : ''} -/* style inject SSR */ -${compiler.template.optimizeSSR ? createInjectorSSR : ''} - -export default __vue_normalize__( - __vue_template__, - __vue_inject_styles__, - typeof __vue_script__ === 'undefined' ? {} : __vue_script__, - __vue_scope_id__, - __vue_is_functional_template__, - __vue_module_identifier__, - typeof __vue_create_injector__ !== 'undefined' ? __vue_create_injector__ : function () {}, - typeof __vue_create_injector_ssr__ !== 'undefined' ? __vue_create_injector_ssr__ : function () {} -)` - - return { code } + let code = + `/* script */\n${script.source.replace( + /export\s+default/, + DEFAULT_EXPORT + )}` + + `\n/* template */\n${template.source + .replace('var render =', 'var __vue_render__ =') + .replace('var staticRenderFns =', 'var __vue_staticRenderFns__ =') + .replace('render._withStripped =', '__vue_render__._withStripped =')} + /* style */ + const __vue_inject_styles__ = ${hasStyle} ? function (inject) { + if (!inject) return + ${styles.map((style, index) => { + const source = IDENTIFIER.test(style.source) + ? style.source + : e(style.source) + const map = !compiler.template.isProduction + ? typeof style.map === 'string' && IDENTIFIER.test(style.map) + ? style.map + : o(style.map) + : undefined + const tokens = + typeof style.module === 'string' && IDENTIFIER.test(style.module) + ? style.module + : o(style.module) + + return ( + (source + ? `inject("${scopeId + + '_' + + index}", { source: ${source}, map: ${map}, media: ${e( + style.media + )} })\n` + : '') + + (style.moduleName + ? `Object.defineProperty(this, "${ + style.moduleName + }", { value: ${tokens} })` + '\n' + : '') + ) + })} + } : undefined + /* scoped */ + const __vue_scope_id__ = ${e(hasScopedStyle) ? e(scopeId) : 'undefined'} + /* module identifier */ + const __vue_module_identifier__ = ${ + compiler.template.optimizeSSR ? e(scopeId) : 'undefined' + } + /* functional template */ + const __vue_is_functional_template__ = ${e(template.functional)} + /* component normalizer */ + ${normalizeComponent} + /* style inject */ + ${!compiler.template.optimizeSSR ? createInjector : ''} + /* style inject SSR */ + ${compiler.template.optimizeSSR ? createInjectorSSR : ''} + + ` + + // generate source map. + { + const input = script.source.split('\n') + + input.forEach((sourceLine, index) => { + if (!sourceLine) return + const matches = /export\s+default/.exec(sourceLine) + if (matches) { + const pos = sourceLine.indexOf(matches[0]) + if (pos > 0) { + mapGenerator.addMapping({ + source: filename, + original: { line: index + 1, column: 0 }, + generated: { line: index + 2, column: 0 } + }) + } + + mapGenerator.addMapping({ + source: filename, + original: { line: index + 1, column: pos }, + generated: { line: index + 2, column: pos } + }) + + if (sourceLine.substr(pos + matches[0].length)) { + mapGenerator.addMapping({ + source: filename, + original: { line: index + 1, column: pos + matches[0].length }, + generated: { line: index + 2, column: pos + DEFAULT_EXPORT.length } + }) + } + } else { + mapGenerator.addMapping({ + source: filename, + original: { line: index + 1, column: 0 }, + generated: { line: index + 2, column: 0 } + }) + } + }) + } + + code += ` + export default __vue_normalize__( + ${ + code.indexOf('__vue_render__') > -1 + ? '{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }' + : '{}' + }, + __vue_inject_styles__, + ${code.indexOf('__vue_script__') > -1 ? '__vue_script__' : '{}'}, + __vue_scope_id__, + __vue_is_functional_template__, + __vue_module_identifier__, + ${ + code.indexOf('__vue_create_injector__') > -1 + ? '__vue_create_injector__' + : 'undefined' + }, + ${ + code.indexOf('__vue_create_injector_ssr__') > -1 + ? '__vue_create_injector_ssr__' + : 'undefined' + } + )` + + if (script.map) { + map = merge(script.map, JSON.parse(mapGenerator.toString())) + } else { + map = JSON.parse(mapGenerator.toString()) + } + + return { code, map } } diff --git a/src/source-map.ts b/src/source-map.ts new file mode 100644 index 0000000..eb20b1e --- /dev/null +++ b/src/source-map.ts @@ -0,0 +1,58 @@ +import { + SourceMapConsumer, + SourceMapGenerator, + RawSourceMap, + MappingItem +} from 'source-map' + +export function merge( + oldMap: RawSourceMap, + newMap: RawSourceMap +): RawSourceMap { + if (!oldMap) return newMap + if (!newMap) return oldMap + + const oldMapConsumer: SourceMapConsumer = new SourceMapConsumer(oldMap) as any + const newMapConsumer: SourceMapConsumer = new SourceMapConsumer(newMap) as any + const mergedMapGenerator = new SourceMapGenerator() + + // iterate on new map and overwrite original position of new map with one of old map + newMapConsumer.eachMapping((mapping: MappingItem) => { + // pass when `originalLine` is null. + // It occurs in case that the node does not have origin in original code. + if (mapping.originalLine == null) return + + const origPosInOldMap = oldMapConsumer.originalPositionFor({ + line: mapping.originalLine, + column: mapping.originalColumn + }) + + if (origPosInOldMap.source == null) return + + mergedMapGenerator.addMapping({ + original: { + line: origPosInOldMap.line, + column: origPosInOldMap.column + }, + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn + }, + source: origPosInOldMap.source, + name: origPosInOldMap.name + } as any) + }) + + const maps = [oldMap, newMap] + const consumers = [oldMapConsumer, newMapConsumer] + consumers.forEach((consumer, index) => { + maps[index].sources.forEach(sourceFile => { + const sourceContent = consumer.sourceContentFor(sourceFile, true) + if (sourceContent !== null) { + mergedMapGenerator.setSourceContent(sourceFile, sourceContent) + } + }) + }) + + return JSON.parse(mergedMapGenerator.toString()) +} diff --git a/test/setup/utils.ts b/test/setup/utils.ts index d8c015f..202bc1d 100644 --- a/test/setup/utils.ts +++ b/test/setup/utils.ts @@ -24,7 +24,9 @@ function inline(filename, code) { if (id === filename) return filename }, load(id) { - if (id === filename) return code + if (id === filename) { + return code + } } } } @@ -55,7 +57,7 @@ function compile(filename, source) { if (style.errors.length) console.error(style.errors) }) - return assemble(compiler, filename, result).code + return assemble(compiler, filename, result) } const babelit = babel({ @@ -147,7 +149,8 @@ async function build(filename) { const vueSource = readFileSync( resolve(__dirname, '../../node_modules/vue/dist/vue.min.js') -) +).toString() +const escape = (any: string) => any.replace(/<\//g, '<\/') async function open(name, browser, code, id = '#test') { const page = await browser.newPage() @@ -159,8 +162,12 @@ async function open(name, browser, code, id = '#test') {
- - + + ` diff --git a/yarn.lock b/yarn.lock index 3956fb9..b4725cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4445,16 +4445,16 @@ source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, sourc version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +source-map@0.6.*, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + source-map@^0.4.2, source-map@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: amdefine ">=0.0.4" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - spdx-correct@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"