Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/preprocessor sourcemaps #5428

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e223c35
add source map support for preprocessors
halfnelson Jun 6, 2020
43c5d5e
preprocessor sourcemaps: prettify code
milahu Sep 5, 2020
b8e9b68
preprocessor sourcemaps: prettify code 2
milahu Sep 5, 2020
4a03b10
preprocessor sourcemaps: prettify code 3
milahu Sep 5, 2020
307276a
handle empty attributes and content
milahu Sep 5, 2020
dee3aab
move fn replace_async, etc
milahu Sep 19, 2020
e753489
fix test/preprocess
milahu Sep 19, 2020
6d06b7b
refactor test/sourcemaps
milahu Sep 19, 2020
433213a
lint commas
milahu Sep 19, 2020
e76f37d
move fn get_replacement
milahu Sep 20, 2020
b7d5974
bugfix in fn merge_tables
milahu Sep 20, 2020
7cccff1
remove hack
milahu Sep 20, 2020
6668f12
trigger test on travis ci
milahu Sep 20, 2020
459dd88
refactor
milahu Sep 21, 2020
880f556
ignore names in sourcemap
milahu Sep 21, 2020
2cf1ae6
handle sourcemap.names
milahu Sep 22, 2020
1073120
remove unnecessary sourcemap encode
milahu Sep 23, 2020
cf600f7
add tests, fix empty map.sources, cleanup gitignore
milahu Sep 23, 2020
38f4ce4
fix decode, dont fix missing map.sources
milahu Sep 24, 2020
47ffc05
optimize concat
milahu Sep 24, 2020
b739bdb
optimize merge_tables, verbose remapper error
milahu Sep 25, 2020
e7abdfa
Merge branch 'master' into feature/preprocessor-sourcemaps
milahu Sep 25, 2020
422cc0d
optimize: use mutable data, unswitch loops
milahu Sep 25, 2020
3d053d9
support default + named import
milahu Sep 26, 2020
a0eb41f
support multiple source files, fix types
milahu Sep 29, 2020
18003d6
fix tests, use decoded mappings, show warnings
milahu Oct 4, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",
"@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-json": "^4.0.1",
"@rollup/plugin-node-resolve": "^6.0.0",
Expand Down Expand Up @@ -89,6 +90,7 @@
"rollup": "^1.27.14",
"source-map": "^0.7.3",
"source-map-support": "^0.5.13",
"sourcemap-codec": "^1.4.8",
"tiny-glob": "^0.2.6",
"tslib": "^1.10.0",
"typescript": "^3.5.3"
Expand Down
30 changes: 30 additions & 0 deletions src/compiler/compile/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import add_to_set from './utils/add_to_set';
import check_graph_for_cycles from './utils/check_graph_for_cycles';
import { print, x, b } from 'code-red';
import { is_reserved_keyword } from './utils/reserved_keywords';
import remapping from '@ampproject/remapping';

interface ComponentOptions {
namespace?: string;
Expand Down Expand Up @@ -324,6 +325,35 @@ export default class Component {
js.map.sourcesContent = [
this.source
];

if (compile_options.sourcemap) {
if (js.map) {
const pre_remap_sources = js.map.sources;
js.map = remapping([js.map, compile_options.sourcemap], () => null);
// remapper can remove our source if it isn't used (no segments map back to it). It is still handy to have a source
// so we add it back
if (js.map.sources && js.map.sources.length == 0) {
js.map.sources = pre_remap_sources;
}
Object.defineProperties(js.map, {
toString: {
enumerable: false,
value: function toString() {
return JSON.stringify(this);
}
},
toUrl: {
enumerable: false,
value: function toUrl() {
return 'data:application/json;charset=utf-8;base64,' + btoa(this.toString());
}
}
});
}
if (css.map) {
css.map = remapping([css.map, compile_options.sourcemap], () => null);
}
}
}

return {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const valid_options = [
'format',
'name',
'filename',
'sourcemap',
'generate',
'outputFilename',
'cssOutputFilename',
Expand Down
1 change: 1 addition & 0 deletions src/compiler/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface CompileOptions {
filename?: string;
generate?: 'dom' | 'ssr' | false;

sourcemap?: object | string;
outputFilename?: string;
cssOutputFilename?: string;
sveltePath?: string;
Expand Down
123 changes: 108 additions & 15 deletions src/compiler/preprocess/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import remapper from '@ampproject/remapping';
import { decode as sourcemap_decode } from 'sourcemap-codec';
import { getLocator } from 'locate-character';
import { StringWithSourcemap, sourcemap_add_offset } from '../utils/string_with_sourcemap';


export interface Processed {
code: string;
map?: object | string;
Expand Down Expand Up @@ -37,12 +43,18 @@ function parse_attributes(str: string) {
interface Replacement {
offset: number;
length: number;
replacement: string;
replacement: StringWithSourcemap;
}

async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) {
async function replace_async(
filename: string,
source: string,
get_location: ReturnType<typeof getLocator>,
re: RegExp,
func: (...any) => Promise<StringWithSourcemap>
): Promise<StringWithSourcemap> {
const replacements: Array<Promise<Replacement>> = [];
str.replace(re, (...args) => {
source.replace(re, (...args) => {
replacements.push(
func(...args).then(
res =>
Expand All @@ -55,18 +67,51 @@ async function replace_async(str: string, re: RegExp, func: (...any) => Promise<
);
return '';
});
let out = '';
let out: StringWithSourcemap;
let last_end = 0;
for (const { offset, length, replacement } of await Promise.all(
replacements
)) {
out += str.slice(last_end, offset) + replacement;
// content = source before replacement
const content = StringWithSourcemap.from_source(
filename, source.slice(last_end, offset), get_location(last_end));
out = out ? out.concat(content) : content;
out = out.concat(replacement);
last_end = offset + length;
}
out += str.slice(last_end);
// final_content = source after last replacement
const final_content = StringWithSourcemap.from_source(
filename, source.slice(last_end), get_location(last_end));
out = out.concat(final_content);
return out;
}

function get_replacement(
filename: string,
offset: number,
get_location: ReturnType<typeof getLocator>,
original: string,
processed: Processed,
prefix: string,
suffix: string
): StringWithSourcemap {
const prefix_with_map = StringWithSourcemap.from_source(
filename, prefix, get_location(offset));
const suffix_with_map = StringWithSourcemap.from_source(
filename, suffix, get_location(offset + prefix.length + original.length));

let processed_map_shifted;
if (processed.map) {
const decoded_map = typeof processed.map === "string" ? JSON.parse(processed.map) : processed.map;
decoded_map.mappings = sourcemap_decode(decoded_map.mappings);
const processed_offset = get_location(offset + prefix.length);
processed_map_shifted = sourcemap_add_offset(decoded_map, processed_offset);
}
const processed_with_map = StringWithSourcemap.from_processed(processed.code, processed_map_shifted);

return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
}

export default async function preprocess(
source: string,
preprocessor: PreprocessorGroup | PreprocessorGroup[],
Expand All @@ -76,60 +121,107 @@ export default async function preprocess(
const filename = (options && options.filename) || preprocessor.filename; // legacy
const dependencies = [];

const preprocessors = Array.isArray(preprocessor) ? preprocessor : [preprocessor];
const preprocessors = preprocessor
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
: []; // noop

const markup = preprocessors.map(p => p.markup).filter(Boolean);
const script = preprocessors.map(p => p.script).filter(Boolean);
const style = preprocessors.map(p => p.style).filter(Boolean);

// sourcemap_list is sorted in reverse order from last map (index 0) to first map (index -1)
// so we use sourcemap_list.unshift() to add new maps
// https://github.com/ampproject/remapping#multiple-transformations-of-a-file
const sourcemap_list: Array<Processed['map']> = [];

for (const fn of markup) {

// run markup preprocessor
const processed = await fn({
content: source,
filename
});

if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
source = processed ? processed.code : source;
if (processed && processed.map) sourcemap_list.unshift(processed.map);
}

for (const fn of script) {
source = await replace_async(
const get_location = getLocator(source);
const res = await replace_async(
filename,
source,
get_location,
/<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
async (match, attributes = '', content = '') => {
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) {
return match;
return no_change();
}
attributes = attributes || '';
content = content || '';

// run script preprocessor
const processed = await fn({
content,
attributes: parse_attributes(attributes),
filename
});

if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<script${attributes}>${processed.code}</script>` : match;
return processed
? get_replacement(filename, offset, get_location, content, processed, `<script${attributes}>`, `</script>`)
: no_change();
}
);
source = res.string;
sourcemap_list.unshift(res.get_sourcemap());
}

for (const fn of style) {
source = await replace_async(
const get_location = getLocator(source);
const res = await replace_async(
filename,
source,
get_location,
/<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
async (match, attributes = '', content = '') => {
async (match, attributes = '', content = '', offset) => {
const no_change = () => StringWithSourcemap.from_source(
filename, match, get_location(offset));
if (!attributes && !content) {
return match;
return no_change();
}
attributes = attributes || '';
content = content || '';

// run style preprocessor
const processed: Processed = await fn({
content,
attributes: parse_attributes(attributes),
filename
});

if (processed && processed.dependencies) dependencies.push(...processed.dependencies);
return processed ? `<style${attributes}>${processed.code}</style>` : match;
return processed
? get_replacement(filename, offset, get_location, content, processed, `<style${attributes}>`, `</style>`)
: no_change();
}
);
source = res.string;
sourcemap_list.unshift(res.get_sourcemap());
}

// https://github.com/ampproject/remapping#usage
// https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap
const map: ReturnType<typeof remapper> =
sourcemap_list.length == 0
? null
: remapper(sourcemap_list as any, () => null, true); // true: skip optional field `sourcesContent`

if (map) delete map.file; // skip optional field `file`

return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
Expand All @@ -138,6 +230,7 @@ export default async function preprocess(

code: source,
dependencies: [...new Set(dependencies)],
map,

toString() {
return source;
Expand Down
Loading