diff --git a/src/compiler/preprocess/index.js b/src/compiler/preprocess/index.js index 55fa878d83..5dd6d48443 100644 --- a/src/compiler/preprocess/index.js +++ b/src/compiler/preprocess/index.js @@ -1,5 +1,10 @@ import { getLocator } from 'locate-character'; -import { MappedCode, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code'; +import { + MappedCode, + parse_attached_sourcemap, + sourcemap_add_offset, + combine_sourcemaps +} from '../utils/mapped_code'; import { decode_map } from './decode_sourcemap.js'; import { replace_in_code, slice_source } from './replace_in_code'; import { regex_whitespaces } from '../utils/patterns'; @@ -10,80 +15,80 @@ const regex_filepath_separator = /[/\\]/; * @param {string} filename */ function get_file_basename(filename) { - return filename.split(regex_filepath_separator).pop(); + return filename.split(regex_filepath_separator).pop(); } /** * Represents intermediate states of the preprocessing. */ class PreprocessResult { - source; - filename; - // 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 + source; + filename; + // 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 - /** - * @default [] - * @type {Array} - */ - sourcemap_list = []; + /** + * @default [] + * @type {Array} + */ + sourcemap_list = []; - /** - * @default [] - * @type {string[]} - */ - dependencies = []; + /** + * @default [] + * @type {string[]} + */ + dependencies = []; - /** - * @type {string} - */ - file_basename = undefined; + /** + * @type {string} + */ + file_basename = undefined; - /** - * @type {Class>} - */ - get_location = undefined; - constructor(source, filename) { - this.source = source; - this.filename = filename; - this.update_source({ string: source }); - // preprocess source must be relative to itself or equal null - this.file_basename = filename == null ? null : get_file_basename(filename); - } + /** + * @type {Class>} + */ + get_location = undefined; + constructor(source, filename) { + this.source = source; + this.filename = filename; + this.update_source({ string: source }); + // preprocess source must be relative to itself or equal null + this.file_basename = filename == null ? null : get_file_basename(filename); + } - /** - * @param {SourceUpdate} - */ - update_source({ string: source, map, dependencies }) { - if (source != null) { - this.source = source; - this.get_location = getLocator(source); - } - if (map) { - this.sourcemap_list.unshift(map); - } - if (dependencies) { - this.dependencies.push(...dependencies); - } - } + /** + * @param {SourceUpdate} + */ + update_source({ string: source, map, dependencies }) { + if (source != null) { + this.source = source; + this.get_location = getLocator(source); + } + if (map) { + this.sourcemap_list.unshift(map); + } + if (dependencies) { + this.dependencies.push(...dependencies); + } + } - /** - * @returns {Processed} - */ - to_processed() { - // Combine all the source maps for each preprocessor function into one - const map = combine_sourcemaps(this.file_basename, this.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: this.source, - dependencies: [...new Set(this.dependencies)], - map: map, - toString: () => this.source - }; - } + /** + * @returns {Processed} + */ + to_processed() { + // Combine all the source maps for each preprocessor function into one + const map = combine_sourcemaps(this.file_basename, this.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: this.source, + dependencies: [...new Set(this.dependencies)], + map, + toString: () => this.source + }; + } } /** * Convert preprocessor output for the tag content into MappedCode @@ -93,24 +98,24 @@ class PreprocessResult { * @returns {MappedCode} */ function processed_content_to_code(processed, location, file_basename) { - // Convert the preprocessed code and its sourcemap to a MappedCode + // Convert the preprocessed code and its sourcemap to a MappedCode - /** - * @type {DecodedSourceMap} - */ - let decoded_map; - if (processed.map) { - decoded_map = decode_map(processed); - // decoded map may not have sources for empty maps like `{ mappings: '' }` - if (decoded_map.sources) { - // offset only segments pointing at original component source - const source_index = decoded_map.sources.indexOf(file_basename); - if (source_index !== -1) { - sourcemap_add_offset(decoded_map, location, source_index); - } - } - } - return MappedCode.from_processed(processed.code, decoded_map); + /** + * @type {DecodedSourceMap} + */ + let decoded_map; + if (processed.map) { + decoded_map = decode_map(processed); + // decoded map may not have sources for empty maps like `{ mappings: '' }` + if (decoded_map.sources) { + // offset only segments pointing at original component source + const source_index = decoded_map.sources.indexOf(file_basename); + if (source_index !== -1) { + sourcemap_add_offset(decoded_map, location, source_index); + } + } + } + return MappedCode.from_processed(processed.code, decoded_map); } /** * Given the whole tag including content, return a `MappedCode` @@ -122,20 +127,25 @@ function processed_content_to_code(processed, location, file_basename) { * @returns {MappedCode} */ function processed_tag_to_code(processed, tag_name, attributes, source) { - const { file_basename, get_location } = source; + const { file_basename, get_location } = source; - /** - * @param {string} code - * @param {number} offset - */ - const build_mapped_code = (code, offset) => MappedCode.from_source(slice_source(code, offset, source)); - const tag_open = `<${tag_name}${attributes || ''}>`; - const tag_close = ``; - const tag_open_code = build_mapped_code(tag_open, 0); - const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length); - parse_attached_sourcemap(processed, tag_name); - const content_code = processed_content_to_code(processed, get_location(tag_open.length), file_basename); - return tag_open_code.concat(content_code).concat(tag_close_code); + /** + * @param {string} code + * @param {number} offset + */ + const build_mapped_code = (code, offset) => + MappedCode.from_source(slice_source(code, offset, source)); + const tag_open = `<${tag_name}${attributes || ''}>`; + const tag_close = ``; + const tag_open_code = build_mapped_code(tag_open, 0); + const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length); + parse_attached_sourcemap(processed, tag_name); + const content_code = processed_content_to_code( + processed, + get_location(tag_open.length), + file_basename + ); + return tag_open_code.concat(content_code).concat(tag_close_code); } const regex_quoted_value = /^['"](.*)['"]$/; @@ -143,16 +153,16 @@ const regex_quoted_value = /^['"](.*)['"]$/; * @param {string} str */ function parse_tag_attributes(str) { - // note: won't work with attribute values containing spaces. - return str - .split(regex_whitespaces) - .filter(Boolean) - .reduce((attrs, attr) => { - const i = attr.indexOf('='); - const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i + 1)] : [attr]; - const [, unquoted] = (value && value.match(regex_quoted_value)) || []; - return { ...attrs, [key]: unquoted ?? value ?? true }; - }, {}); + // note: won't work with attribute values containing spaces. + return str + .split(regex_whitespaces) + .filter(Boolean) + .reduce((attrs, attr) => { + const i = attr.indexOf('='); + const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i + 1)] : [attr]; + const [, unquoted] = (value && value.match(regex_quoted_value)) || []; + return { ...attrs, [key]: unquoted ?? value ?? true }; + }, {}); } const regex_style_tags = /|([^]*?)<\/style>|\/>)/gi; const regex_script_tags = /|([^]*?)<\/script>|\/>)/gi; @@ -164,39 +174,41 @@ const regex_script_tags = /|([^]*?)<\/script>|\ * @returns {Promise} */ async function process_tag(tag_name, preprocessor, source) { - const { filename, source: markup } = source; - const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags; + const { filename, source: markup } = source; + const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags; - /** - * @type {string[]} - */ - const dependencies = []; + /** + * @type {string[]} + */ + const dependencies = []; - /** - * @param {string} tag_with_content - * @param {number} tag_offset - * @returns {Promise} - */ - async function process_single_tag(tag_with_content, attributes = '', content = '', tag_offset) { - const no_change = () => MappedCode.from_source(slice_source(tag_with_content, tag_offset, source)); - if (!attributes && !content) - return no_change(); - const processed = await preprocessor({ - content: content || '', - attributes: parse_tag_attributes(attributes || ''), - markup, - filename - }); - if (!processed) - return no_change(); - if (processed.dependencies) - dependencies.push(...processed.dependencies); - if (!processed.map && processed.code === content) - return no_change(); - return processed_tag_to_code(processed, tag_name, attributes, slice_source(content, tag_offset, source)); - } - const { string, map } = await replace_in_code(tag_regex, process_single_tag, source); - return { string, map, dependencies }; + /** + * @param {string} tag_with_content + * @param {number} tag_offset + * @returns {Promise} + */ + async function process_single_tag(tag_with_content, attributes = '', content = '', tag_offset) { + const no_change = () => + MappedCode.from_source(slice_source(tag_with_content, tag_offset, source)); + if (!attributes && !content) return no_change(); + const processed = await preprocessor({ + content: content || '', + attributes: parse_tag_attributes(attributes || ''), + markup, + filename + }); + if (!processed) return no_change(); + if (processed.dependencies) dependencies.push(...processed.dependencies); + if (!processed.map && processed.code === content) return no_change(); + return processed_tag_to_code( + processed, + tag_name, + attributes, + slice_source(content, tag_offset, source) + ); + } + const { string, map } = await replace_in_code(tag_regex, process_single_tag, source); + return { string, map, dependencies }; } /** @@ -204,25 +216,24 @@ async function process_tag(tag_name, preprocessor, source) { * @param {Source} source */ async function process_markup(process, source) { - const processed = await process({ - content: source.source, - filename: source.filename - }); - if (processed) { - return { - string: processed.code, - map: processed.map - ? // TODO: can we use decode_sourcemap? - typeof processed.map === 'string' - ? JSON.parse(processed.map) - : processed.map - : undefined, - dependencies: processed.dependencies - }; - } - else { - return {}; - } + const processed = await process({ + content: source.source, + filename: source.filename + }); + if (processed) { + return { + string: processed.code, + map: processed.map + ? // TODO: can we use decode_sourcemap? + typeof processed.map === 'string' + ? JSON.parse(processed.map) + : processed.map + : undefined, + dependencies: processed.dependencies + }; + } else { + return {}; + } } /** @@ -232,39 +243,35 @@ async function process_markup(process, source) { * @returns {Promise} */ export default async function preprocess(source, preprocessor, options) { - - /** - * @type {string | undefined} - */ - const filename = (options && options.filename) || preprocessor.filename; // legacy - 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); - const result = new PreprocessResult(source, filename); - // TODO keep track: what preprocessor generated what sourcemap? - // to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings - for (const process of markup) { - result.update_source(await process_markup(process, result)); - } - for (const process of script) { - result.update_source(await process_tag('script', process, result)); - } - for (const preprocess of style) { - result.update_source(await process_tag('style', preprocess, result)); - } - return result.to_processed(); + /** + * @type {string | undefined} + */ + const filename = (options && options.filename) || preprocessor.filename; // legacy + 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); + const result = new PreprocessResult(source, filename); + // TODO keep track: what preprocessor generated what sourcemap? + // to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings + for (const process of markup) { + result.update_source(await process_markup(process, result)); + } + for (const process of script) { + result.update_source(await process_tag('script', process, result)); + } + for (const preprocess of style) { + result.update_source(await process_tag('style', preprocess, result)); + } + return result.to_processed(); } - - - /** @typedef {Object} SourceUpdate * @property {string} [string] * @property {DecodedSourceMap} [map] - * @property {string[]} [dependencies] - */ \ No newline at end of file + * @property {string[]} [dependencies] + */