mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
9.3 KiB
315 lines
9.3 KiB
import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types/types';
|
|
import { decode as decode_mappings } from 'sourcemap-codec';
|
|
import { getLocator } from 'locate-character';
|
|
import { StringWithSourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/string_with_sourcemap';
|
|
|
|
export interface Processed {
|
|
code: string;
|
|
map?: string | object; // we are opaque with the type here to avoid dependency on the remapping module for our public types.
|
|
dependencies?: string[];
|
|
}
|
|
|
|
export interface PreprocessorGroup {
|
|
markup?: (options: {
|
|
content: string;
|
|
filename: string;
|
|
}) => Processed | Promise<Processed>;
|
|
style?: Preprocessor;
|
|
script?: Preprocessor;
|
|
}
|
|
|
|
export type Preprocessor = (options: {
|
|
content: string;
|
|
attributes: Record<string, string | boolean>;
|
|
filename?: string;
|
|
}) => Processed | Promise<Processed>;
|
|
|
|
function parse_attributes(str: string) {
|
|
const attrs = {};
|
|
str.split(/\s+/).filter(Boolean).forEach(attr => {
|
|
const p = attr.indexOf('=');
|
|
if (p === -1) {
|
|
attrs[attr] = true;
|
|
} else {
|
|
attrs[attr.slice(0, p)] = '\'"'.includes(attr[p + 1]) ?
|
|
attr.slice(p + 2, -1) :
|
|
attr.slice(p + 1);
|
|
}
|
|
});
|
|
return attrs;
|
|
}
|
|
|
|
interface Replacement {
|
|
offset: number;
|
|
length: number;
|
|
replacement: StringWithSourcemap;
|
|
}
|
|
|
|
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>> = [];
|
|
source.replace(re, (...args) => {
|
|
replacements.push(
|
|
func(...args).then(
|
|
res =>
|
|
({
|
|
offset: args[args.length - 2],
|
|
length: args[0].length,
|
|
replacement: res
|
|
}) as Replacement
|
|
)
|
|
);
|
|
return '';
|
|
});
|
|
const out = new StringWithSourcemap();
|
|
let last_end = 0;
|
|
for (const { offset, length, replacement } of await Promise.all(
|
|
replacements
|
|
)) {
|
|
// content = unchanged source characters before the replaced segment
|
|
const content = StringWithSourcemap.from_source(
|
|
filename, source.slice(last_end, offset), get_location(last_end));
|
|
out.concat(content).concat(replacement);
|
|
last_end = offset + length;
|
|
}
|
|
// final_content = unchanged source characters after last replaced segment
|
|
const final_content = StringWithSourcemap.from_source(
|
|
filename, source.slice(last_end), get_location(last_end));
|
|
return out.concat(final_content);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* import decoded sourcemap from mozilla/source-map/SourceMapGenerator
|
|
* forked from source-map/lib/source-map-generator.js
|
|
* from methods _serializeMappings and toJSON
|
|
* we cannot use source-map.d.ts types, cos we access hidden properties
|
|
*/
|
|
function decoded_sourcemap_from_generator(generator: any) {
|
|
function areEqualMappings(a, b) {
|
|
return (
|
|
// sorted by selectivity
|
|
a.generatedColumn == b.generatedColumn &&
|
|
a.originalColumn == b.originalColumn &&
|
|
a.name == b.name &&
|
|
a.generatedLine == b.generatedLine &&
|
|
a.originalLine == b.originalLine &&
|
|
a.source == b.source
|
|
);
|
|
}
|
|
function convertMappings() {
|
|
// use global variable: generator
|
|
let previousGeneratedLine = 1;
|
|
const result = [[]];
|
|
let resultLine;
|
|
let resultSegment;
|
|
let mapping;
|
|
|
|
const sourceIdx = generator._sources.toArray()
|
|
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
|
|
|
|
const nameIdx = generator._names.toArray()
|
|
.reduce((acc, val, idx) => (acc[val] = idx, acc), {});
|
|
|
|
const mappings = generator._mappings.toArray();
|
|
resultLine = result[0];
|
|
|
|
for (let i = 0, len = mappings.length; i < len; i++) {
|
|
mapping = mappings[i];
|
|
|
|
if (mapping.generatedLine > previousGeneratedLine) {
|
|
while (mapping.generatedLine > previousGeneratedLine) {
|
|
result.push([]);
|
|
previousGeneratedLine++;
|
|
}
|
|
resultLine = result[mapping.generatedLine - 1]; // line is one-based
|
|
} else if (i > 0) {
|
|
if (areEqualMappings(mapping, mappings[i - 1])) {
|
|
continue;
|
|
}
|
|
}
|
|
resultLine.push([mapping.generatedColumn]);
|
|
resultSegment = resultLine[resultLine.length - 1];
|
|
|
|
if (mapping.source != null) {
|
|
resultSegment.push(...[
|
|
sourceIdx[mapping.source],
|
|
mapping.originalLine - 1, // line is one-based
|
|
mapping.originalColumn
|
|
]);
|
|
if (mapping.name != null) {
|
|
resultSegment.push(nameIdx[mapping.name]);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
const map = {
|
|
version: generator._version,
|
|
sources: generator._sources.toArray(),
|
|
names: generator._names.toArray(),
|
|
mappings: convertMappings()
|
|
};
|
|
if (generator._file != null) {
|
|
(map as any).file = generator._file;
|
|
}
|
|
// not needed: map.sourcesContent and map.sourceRoot
|
|
return map;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Convert a preprocessor output and its leading prefix and trailing suffix into StringWithSourceMap
|
|
*/
|
|
function get_replacement(
|
|
filename: string,
|
|
offset: number,
|
|
get_location: ReturnType<typeof getLocator>,
|
|
original: string,
|
|
processed: Processed,
|
|
prefix: string,
|
|
suffix: string
|
|
): StringWithSourcemap {
|
|
|
|
// Convert the unchanged prefix and suffix to 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));
|
|
|
|
// Convert the preprocessed code and its sourcemap to a StringWithSourcemap
|
|
let decoded_map: DecodedSourceMap;
|
|
if (processed.map) {
|
|
decoded_map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
|
|
if (typeof(decoded_map.mappings) === 'string') {
|
|
decoded_map.mappings = decode_mappings(decoded_map.mappings);
|
|
}
|
|
if ((decoded_map as any)._mappings && decoded_map.constructor.name == 'SourceMapGenerator') {
|
|
// import decoded sourcemap from mozilla/source-map/SourceMapGenerator
|
|
decoded_map = decoded_sourcemap_from_generator(decoded_map);
|
|
}
|
|
sourcemap_add_offset(decoded_map, get_location(offset + prefix.length));
|
|
}
|
|
const processed_with_map = StringWithSourcemap.from_processed(processed.code, decoded_map);
|
|
|
|
// Surround the processed code with the prefix and suffix, retaining valid sourcemappings
|
|
return prefix_with_map.concat(processed_with_map).concat(suffix_with_map);
|
|
}
|
|
|
|
export default async function preprocess(
|
|
source: string,
|
|
preprocessor: PreprocessorGroup | PreprocessorGroup[],
|
|
options?: { filename?: string }
|
|
) {
|
|
// @ts-ignore todo: doublecheck
|
|
const filename = (options && options.filename) || preprocessor.filename; // legacy
|
|
const dependencies = [];
|
|
|
|
const preprocessors = preprocessor
|
|
? Array.isArray(preprocessor) ? preprocessor : [preprocessor]
|
|
: [];
|
|
|
|
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<DecodedSourceMap | RawSourceMap> = [];
|
|
|
|
// TODO keep track: what preprocessor generated what sourcemap? to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings
|
|
|
|
for (const fn of markup) {
|
|
|
|
// run markup preprocessor
|
|
const processed = await fn({
|
|
content: source,
|
|
filename
|
|
});
|
|
|
|
if (!processed) continue;
|
|
|
|
if (processed.dependencies) dependencies.push(...processed.dependencies);
|
|
source = processed.code;
|
|
if (processed.map) {
|
|
sourcemap_list.unshift(
|
|
typeof(processed.map) === 'string'
|
|
? JSON.parse(processed.map)
|
|
: processed.map
|
|
);
|
|
}
|
|
}
|
|
|
|
async function preprocess_tag_content(tag_name: 'style' | 'script', preprocessor: Preprocessor) {
|
|
const get_location = getLocator(source);
|
|
const tag_regex = tag_name == 'style'
|
|
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
|
|
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
|
|
|
|
const res = await replace_async(
|
|
filename,
|
|
source,
|
|
get_location,
|
|
tag_regex,
|
|
async (match, attributes = '', content = '', offset) => {
|
|
const no_change = () => StringWithSourcemap.from_source(
|
|
filename, match, get_location(offset));
|
|
if (!attributes && !content) {
|
|
return no_change();
|
|
}
|
|
attributes = attributes || '';
|
|
content = content || '';
|
|
|
|
// run script preprocessor
|
|
const processed = await preprocessor({
|
|
content,
|
|
attributes: parse_attributes(attributes),
|
|
filename
|
|
});
|
|
|
|
if (!processed) return no_change();
|
|
if (processed.dependencies) dependencies.push(...processed.dependencies);
|
|
return get_replacement(filename, offset, get_location, content, processed, `<${tag_name}${attributes}>`, `</${tag_name}>`);
|
|
}
|
|
);
|
|
source = res.string;
|
|
sourcemap_list.unshift(res.map);
|
|
}
|
|
|
|
for (const fn of script) {
|
|
await preprocess_tag_content('script', fn);
|
|
}
|
|
|
|
for (const fn of style) {
|
|
await preprocess_tag_content('style', fn);
|
|
}
|
|
|
|
// Combine all the source maps for each preprocessor function into one
|
|
const map: RawSourceMap = combine_sourcemaps(
|
|
filename,
|
|
sourcemap_list
|
|
);
|
|
|
|
return {
|
|
// TODO return separated output, in future version where svelte.compile supports it:
|
|
// style: { code: styleCode, map: styleMap },
|
|
// script { code: scriptCode, map: scriptMap },
|
|
// markup { code: markupCode, map: markupMap },
|
|
|
|
code: source,
|
|
dependencies: [...new Set(dependencies)],
|
|
map: (map as object),
|
|
toString() {
|
|
return source;
|
|
}
|
|
};
|
|
}
|