chromium/third_party/material_web_components/rewrite_imports.py

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
import os
import re
import sys
import json

from pathlib import Path

_HERE_DIR = Path(__file__).parent
_SOURCE_MAP_CREATOR = (_HERE_DIR / 'rewrite_imports.mjs').resolve()

_NODE_PATH = (_HERE_DIR.parent.parent / 'third_party' / 'node').resolve()
sys.path.append(str(_NODE_PATH))
import node

_CWD = os.getcwd()

# TODO(crbug.com/1320176): Consider either integrating this functionality into
# ts_library.py or replacing the regex if only "tslib" is ever rewritten.
def main(argv):
    parser = argparse.ArgumentParser()
    # List of imports and what they should be rewritten to. Specified as an
    # array of "src|dest" strings. Note that if the src is not a directory, it
    # will be treated as a regex matched against the entire import path. I.e.
    # "foo|bar" will translate to re.sub("^foo$", "bar", line). Since re.sub is
    # used, regex features like referencing groups from `src` in `dest` are
    # available.
    parser.add_argument('--import_mappings', nargs='*')
    # List of rules for renaming imported variables. The format is
    # "path:oldName|newName". When "path" is part of the original path, the
    # variable is renamed to "newName". E.g.
    # Rule: 'lit/static-html:html|staticHtml'
    # Original: import { static } from 'lit/static-html'
    # Rewrite: import { staticHtml } from 'lit/static-html'
    parser.add_argument('--import_var_mappings', nargs='*')
    # The directory to output the rewritten files to.
    parser.add_argument('--out_dir', required=True)
    # The directory to output the manifest file to. The manifest can be used
    # by downstream tools (such as generate_grd) to capture all files that were
    # rewritten.
    parser.add_argument('--manifest_out', required=True)
    # The directory all in_files are under, used to construct a real path on
    # disk via base_dir + in_file.
    parser.add_argument('--base_dir', required=True)
    # List of files to rewrite imports for, all files should be provided
    # relative to base_dir. Each files path is preserved when outputed
    # into out_dir. I.e. "a/b/c/foo" will be outputted to "base_dir/a/b/c/foo".
    parser.add_argument('--in_files', nargs='*')
    args = parser.parse_args(argv)

    manifest = {'base_dir': args.out_dir, 'files': []}
    import_mappings = dict()
    for mapping in args.import_mappings:
        (src, dst) = mapping.split('|')
        import_mappings[src] = dst

    import_var_mappings = list()
    for mapping in args.import_var_mappings:
        (path, renaming) = mapping.split(':')
        (old_name, new_name) = renaming.split('|')
        import_var_mappings.append((path, (old_name, new_name)))

    # For `import_path`, either replace the prefix as described in
    #  `--import_mappings` or drop the "generated:" prefix.
    def _map_import(import_path):
        for regex, substitution in import_mappings.items():
            if regex[-1] == "/":
                substitution = rf"{substitution}\g<file>"
            rewritten = re.sub(rf"^{regex}(?P<file>.*)", substitution,
                               import_path)
            if rewritten != import_path:
                return rewritten

        return re.sub(r'^generated:(.*)', r'\g<1>', import_path)

    # Applies the rules from --import_var_mappings and returns the rewritten
    # import variables.
    def _map_import_vars(path, variables):
        for (map_path, (old, new)) in import_var_mappings:
            if map_path in path:
                # Rewrite direct imports:
                # import {html} from "lit/static-html" -->
                # import {staticHtml as html} from "lit/static-html"
                variables = re.sub(rf"\b({old})(\s*[,}}])", rf"{new} as \1\2",
                                   variables)
                # Rewrite aliased imports:
                # import {html as foo} from "lit/static-html" -->
                # import {staticHtml as foo} from "lit/static-html"
                variables = re.sub(rf"\b{old} (as \w+)", rf"{new} \1",
                                   variables)
                # Cleanup aliases:
                # import {staticHtml as staticHtml} from "lit/static-html" -->
                # import {staticHtml} from "lit/static-html"
                variables = variables.replace(f'{new} as {new}', new)
        return variables

    for f in args.in_files:
        output = []
        changed_lines_list = list()
        for line_no, line in enumerate(
                open(os.path.join(args.base_dir, f), 'r').readlines()):
            # Investigate JS parsing if this is insufficient.
            match = re.match(r'^(import .*["\'])(.*)(["\'];)$', line)
            if match:
                import_vars = match.group(1)
                import_path = match.group(2)
                new_import_path = _map_import(import_path)
                new_import_vars = _map_import_vars(import_path, import_vars)
                # If this is an import statement line and it has a replacement,
                # modify the line before outputing it.
                if new_import_path != import_path or new_import_vars != import_vars:
                    line = f"{new_import_vars}{new_import_path}{match.group(3)}\n"
                    generated_column = len(import_vars) + len(import_path)
                    # TODO(b/290142486): Also adjust the location of import var
                    # tokens.
                    rewritten_column = len(new_import_vars) + len(
                        new_import_path)
                    changed_line = {
                        "lineNum": line_no,
                        "generatedColumn": generated_column,
                        "rewrittenColumn": rewritten_column
                    }
                    changed_lines_list.append(changed_line)
            output.append(line)
        with open(os.path.join(args.out_dir, f), 'w') as out_file:
            out_file.write(''.join(output))
        manifest['files'].append(f)

        node.RunNode([str(_SOURCE_MAP_CREATOR), args.base_dir, f, args.out_dir, json.dumps(changed_lines_list)])

    with open(args.manifest_out, 'w') as manifest_file:
        json.dump(manifest, manifest_file)


if __name__ == '__main__':
    main(sys.argv[1:])