diff --git a/src/cli/compile.ts b/src/cli/compile.ts index 47141f234649..54f728647956 100644 --- a/src/cli/compile.ts +++ b/src/cli/compile.ts @@ -107,6 +107,7 @@ function compileFile(input, output, options) { let compiled; try { + delete options.sourceMap; // compiler doesn't use this option compiled = svelte.compile(source, options); } catch (err) { error(err); diff --git a/src/compile/index.ts b/src/compile/index.ts index 59e204b81630..ad9522bf3eae 100644 --- a/src/compile/index.ts +++ b/src/compile/index.ts @@ -1,3 +1,4 @@ +import { SourceMapGenerator, SourceMapConsumer } from 'source-map'; import { assign } from '../shared'; import Stats from '../Stats'; import parse from '../parse/index'; @@ -6,6 +7,7 @@ import renderSSR from './render-ssr/index'; import { CompileOptions, Warning, Ast } from '../interfaces'; import Component from './Component'; import deprecate from '../utils/deprecate'; +import { relative } from 'path'; function normalize_options(options: CompileOptions): CompileOptions { let normalized = assign({ generate: 'dom', dev: false }, options); @@ -89,7 +91,32 @@ export default function compile(source: string, options: CompileOptions = {}) { return renderSSR(component, options); } - return renderDOM(component, options); + const result = renderDOM(component, options); + + if (options.sourceMap) { + const cwd = process.cwd(); + const inputConsumer = new SourceMapConsumer(options.sourceMap); + + result.js.map.sources = result.js.map.sources.map(file => + relative(cwd, file) + ); + const jsSourceMap = SourceMapGenerator.fromSourceMap( + new SourceMapConsumer(result.js.map) + ); + jsSourceMap.applySourceMap(inputConsumer); + result.js.map = jsSourceMap.toJSON(); + + result.css.map.sources = result.css.map.sources.map(file => + relative(cwd, file) + ); + const cssSourceMap = SourceMapGenerator.fromSourceMap( + new SourceMapConsumer(result.css.map) + ); + cssSourceMap.applySourceMap(inputConsumer); + result.css.map = cssSourceMap.toJSON(); + } + + return result; } catch (err) { onerror(err); } diff --git a/src/interfaces.ts b/src/interfaces.ts index b637a727bbae..8609576841d7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -57,6 +57,7 @@ export interface CompileOptions { legacy?: boolean; customElement?: CustomElementOptions | true; css?: boolean; + sourceMap?: string | object; preserveComments?: boolean | false; diff --git a/src/preprocess/index.ts b/src/preprocess/index.ts index 047b124991bc..e72c1c3cecdb 100644 --- a/src/preprocess/index.ts +++ b/src/preprocess/index.ts @@ -1,4 +1,7 @@ import { SourceMap } from 'magic-string'; +import { SourceMapConsumer, SourceMapGenerator } from 'source-map'; +import getCodeFrame from '../utils/getCodeFrame.js'; +import { relative } from 'path'; export interface PreprocessOptions { markup?: (options: { @@ -31,6 +34,24 @@ function parseAttributes(str: string) { return attrs; } +function addMappings(generator: SourceMapConsumer, map: string | object) { + const consumer = new SourceMapConsumer(map); + consumer.eachMapping(mapping => { + generator.addMapping({ + source: mapping.source, + name: mapping.name, + original: { + line: mapping.originalLine, + column: mapping.originalColumn + }, + generated: { + line: mapping.generatedLine, + column: mapping.generatedColumn + } + }); + }); +} + async function replaceTagContents( source, type: 'script' | 'style', @@ -43,22 +64,74 @@ async function replaceTagContents( if (match) { const attributes: Record = parseAttributes(match[1]); const content: string = match[2]; - const processed: { code: string, map?: SourceMap | string } = await preprocessor({ - content, - attributes, - filename : options.filename - }); - if (processed && processed.code) { - return ( - source.slice(0, match.index) + - `<${type}>${processed.code}` + - source.slice(match.index + match[0].length) - ); + // Line number of the match + let line = 0; + for (let i = 0; i <= match.index; i = source.indexOf('\n', i + 1)) { + line++; + } + + line--; + + try { + const processed: { + code: string; + map?: SourceMap | string; + } = await preprocessor({ + content, + attributes, + filename: options.filename, + }); + + if (processed && processed.code) { + const code = + source.slice(0, match.index) + + `<${type}>${processed.code}` + + source.slice(match.index + match[0].length); + + // Shift sourcemap to the appropriate line + if (processed.map) { + const consumer = new SourceMapConsumer(processed.map); + const generator = new SourceMapGenerator( + options.filename + ? { file: relative(process.cwd(), options.filename) } + : {} + ); + consumer.eachMapping(mapping => { + generator.addMapping({ + source: mapping.source, + name: mapping.name, + original: { + line: mapping.originalLine + line, + column: mapping.originalColumn, + }, + generated: { + line: mapping.generatedLine + line, + column: mapping.generatedColumn, + }, + }); + }); + + return { code, map: generator.toJSON() }; + } + + return { code }; + } + } catch (err) { + if (err.line && err.column) { + err.frame = getCodeFrame(source, line + err.line - 1, err.column); + } else if (err.start && err.start.line && err.start.column) { + err.frame = getCodeFrame(source, line + err.start.line - 1, err.column); + } else if (typeof err.start === 'number') { + const start = locate(contents, err.start, { offsetLine: 1 }); + err.frame = getCodeFrame(source, line + start.line - 1, start.column); + } + + throw err; } } - return source; + return { code: source }; } export default async function preprocess( @@ -67,24 +140,62 @@ export default async function preprocess( ) { const { markup, style, script } = options; + + let markupMap: SourceMapGenerator; + if (!!markup) { - const processed: { - code: string, - map?: SourceMap | string - } = await markup({ - content: source, - filename: options.filename - }); + try { + const processed: { + code: string; + map?: SourceMap | string; + } = await markup({ + content: source, + filename: options.filename, + }); + + source = processed.code; + if (processed.map) { + markupMap = SourceMapGenerator.fromSourceMap( + new SourceMapConsumer(processed.map) + ); + } + } catch (err) { + if (err.line && err.column) { + err.frame = getCodeFrame(source, err.line - 1, err.column); + } else if (err.start && err.start.line && err.start.column) { + err.frame = getCodeFrame(source, err.start.line - 1, err.column); + } else if (typeof err.start === 'number') { + const start = locate(source, err.start, { offsetLine: 1 }); + err.frame = getCodeFrame(source, start.line - 1, start.column); + } - source = processed.code; + throw err; + } } + let allMaps = new SourceMapGenerator( + options.filename ? { file: relative(process.cwd(), options.filename) } : {} + ); + if (!!style) { - source = await replaceTagContents(source, 'style', style, options); + const { code, map } = await replaceTagContents(source, 'style', style, options); + source = code; + if (map) { + addMappings(allMaps, map); + } } if (!!script) { - source = await replaceTagContents(source, 'script', script, options); + const { code, map } = await replaceTagContents(source, 'script', script, options); + source = code; + if (map) { + addMappings(allMaps, map); + } + } + + if (markupMap) { + markupMap.applySourceMap(new SourceMapConsumer(allMaps.toString())); + allMaps = markupMap; } return { @@ -95,6 +206,10 @@ export default async function preprocess( toString() { return source; + }, + + getMap() { + return allMaps.toJSON(); } }; } \ No newline at end of file