From a172a754276aae53419eb3e34e2ae6c651bcbd1f Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 11 May 2023 17:05:33 +0200 Subject: [PATCH] Convert src/compiler/preprocess/index.ts to JavaScript --- src/compiler/preprocess/index.ts | 455 ++++++++++++++++--------------- 1 file changed, 230 insertions(+), 225 deletions(-) diff --git a/src/compiler/preprocess/index.ts b/src/compiler/preprocess/index.ts index d394eb39c9..55fa878d83 100644 --- a/src/compiler/preprocess/index.ts +++ b/src/compiler/preprocess/index.ts @@ -1,265 +1,270 @@ -import type { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping'; import { getLocator } from 'locate-character'; -import { - MappedCode, - SourceLocation, - parse_attached_sourcemap, - sourcemap_add_offset, - combine_sourcemaps -} from '../utils/mapped_code'; -import { decode_map } from './decode_sourcemap'; +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 { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types'; import { regex_whitespaces } from '../utils/patterns'; - export * from './types'; - -interface SourceUpdate { - string?: string; - map?: DecodedSourceMap; - dependencies?: string[]; -} - const regex_filepath_separator = /[/\\]/; -function get_file_basename(filename: string) { - return filename.split(regex_filepath_separator).pop(); +/** + * @param {string} filename + */ +function get_file_basename(filename) { + return filename.split(regex_filepath_separator).pop(); } - /** * Represents intermediate states of the preprocessing. */ -class PreprocessResult implements Source { - // 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 - sourcemap_list: Array = []; - dependencies: string[] = []; - file_basename: string; - - get_location: ReturnType; - - constructor(public source: string, public filename?: string) { - 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); - } - - update_source({ string: source, map, dependencies }: SourceUpdate) { - if (source != null) { - this.source = source; - this.get_location = getLocator(source); - } - - if (map) { - this.sourcemap_list.unshift(map); - } - - if (dependencies) { - this.dependencies.push(...dependencies); - } - } - - to_processed(): 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 }, +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 + + /** + * @default [] + * @type {Array} + */ + sourcemap_list = []; + + /** + * @default [] + * @type {string[]} + */ + dependencies = []; + + /** + * @type {string} + */ + file_basename = undefined; - code: this.source, - dependencies: [...new Set(this.dependencies)], - map: map as object, - toString: () => this.source - }; - } + /** + * @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); + } + } + + /** + * @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 + }; + } } - /** * Convert preprocessor output for the tag content into MappedCode + * @param {Processed} processed + * @param {SourceLocation} location + * @param {string} file_basename + * @returns {MappedCode} */ -function processed_content_to_code( - processed: Processed, - location: SourceLocation, - file_basename: string -): MappedCode { - // Convert the preprocessed code and its sourcemap to a MappedCode - let decoded_map: DecodedSourceMap; - 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); - } - } - } +function processed_content_to_code(processed, location, file_basename) { + // Convert the preprocessed code and its sourcemap to a MappedCode - 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` * representing the tag content replaced with `processed`. + * @param {Processed} processed + * @param {'style' | 'script'} tag_name + * @param {string} attributes + * @param {Source} source + * @returns {MappedCode} */ -function processed_tag_to_code( - processed: Processed, - tag_name: 'style' | 'script', - attributes: string, - source: Source -): MappedCode { - const { file_basename, get_location } = source; - - const build_mapped_code = (code: string, offset: number) => - 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); +function processed_tag_to_code(processed, tag_name, attributes, 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); } - const regex_quoted_value = /^['"](.*)['"]$/; -function parse_tag_attributes(str: string) { - // 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 }; - }, {}); +/** + * @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 }; + }, {}); } - const regex_style_tags = /|([^]*?)<\/style>|\/>)/gi; const regex_script_tags = /|([^]*?)<\/script>|\/>)/gi; - /** * Calculate the updates required to process all instances of the specified tag. + * @param {'style' | 'script'} tag_name + * @param {Preprocessor} preprocessor + * @param {Source} source + * @returns {Promise} */ -async function process_tag( - tag_name: 'style' | 'script', - preprocessor: Preprocessor, - source: Source -): Promise { - const { filename, source: markup } = source; - const tag_regex = tag_name === 'style' ? regex_style_tags : regex_script_tags; - - const dependencies: string[] = []; - - async function process_single_tag( - tag_with_content: string, - attributes = '', - content = '', - tag_offset: number - ): Promise { - 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) - ); - } +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 { string, map } = await replace_in_code(tag_regex, process_single_tag, source); - - return { string, map, 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 }; } -async function process_markup(process: MarkupPreprocessor, source: 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 {}; - } +/** + * @param {MarkupPreprocessor} process + * @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 {}; + } } -export default async function preprocess( - source: string, - preprocessor: PreprocessorGroup | PreprocessorGroup[], - options?: { filename?: string } -): Promise { - const filename: string | undefined = - (options && options.filename) || (preprocessor as any).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); +/** + * @param {string} source + * @param {PreprocessorGroup | PreprocessorGroup[]} preprocessor + * @param {{ filename?: string }} options + * @returns {Promise} + */ +export default async function preprocess(source, preprocessor, options) { - // TODO keep track: what preprocessor generated what sourcemap? - // to make debugging easier = detect low-resolution sourcemaps in fn combine_mappings + /** + * @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(); +} - 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