#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import re import argparse styled_component_regex_no_capture = re.compile('const \w+ = styled\.\w+`.+?`;', re.DOTALL) styled_component_regex_capture = re.compile('const (\w+) = styled\.(\w+)`(.+)`;', re.DOTALL) style_identifiers = re.compile('\.\s*([\w]+[\w-]*)+') class_name_attribute_capture = re.compile('className=(["{])`?([\w\s-]+?)`?(["}])') class_name_attribute_no_capture = re.compile('className=["{]`?[\w\s-]+?`?["}]') style_file_import = 'styleManual' style_file_name = style_file_import + '.module.scss' def lower_first(str): return str[0].lower() + str[1:] def class_name_to_camel_case(string): if '-' not in string and '_' not in string: return lower_first(string) no_double_dash = string.replace('--', '-') camel_case = ''.join(x.capitalize() or '_' for x in no_double_dash.split('-')) return lower_first(camel_case) def transform_class_name_attribute(class_name_attribute, class_names_replacement): group = class_name_attribute_capture.match(class_name_attribute).groups() classes_in_group = group[1].split(' ') classes_transformed = [] for class_in_group in classes_in_group: if class_in_group in class_names_replacement: classes_transformed.append('${' + style_file_import + "." + class_names_replacement[class_in_group] + '}') else: print("! Warning: using global class", class_in_group) classes_transformed.append(class_in_group) class_names_transformed = ' '.join(classes_transformed) transformed = "className={`" + class_names_transformed + "`}" return transformed # Transforms a component by extracting the css into 2 strings def transform_component(component_text, verbose=False): group = styled_component_regex_capture.match(component_text) component_name = group[1].strip() component_type = group[2].strip() component_style = group[3].strip() # In case the component uses $(props) if "=>" in component_text: if verbose: print('> Skipping ' + component_name + ' because contains props') return # In case the component uses ${Mixing} if "${" in component_text: if verbose: print('> Skipping ' + component_name + ' because contains mixins') return if verbose: print('> Transforming ' + component_name) component_style_name = lower_first(component_name) component = """const {component_name} = (props) => {{ const {{children}} = props; return (<{component_type} {{...props}} className={{{style_file_import}.{component_style_name}}}> {{children}} </{component_type}>); }};""".format(component_name=component_name, style_file_import=style_file_import, component_style_name=component_style_name, component_type=component_type) component_style = """.{component_style_name} {{ {component_style} }} """.format(component_style_name=component_style_name, component_style=component_style) return { "component": component, "component_text": component_text, "component_style_name": component_style_name, "component_style": component_style } def handle_file(file_path, components, verbose=False, dry_run=False): if "app/pages" in file_path: return all_styles = [] components_replacement = [] all_styles_class_names_replacement = {} for component in components: component_style = component["component_style"] styles_class_names = style_identifiers.findall(component_style) for styles_class_name in styles_class_names: all_styles_class_names_replacement[styles_class_name] = class_name_to_camel_case(styles_class_name) all_styles.append(component_style) components_replacement.append([component["component_text"], component["component"], component_style]) if verbose and False: for replacement in components_replacement: print('Replacing: ---') print() print(replacement[0]) print() print('---') print() print('jsx') print() print(replacement[1]) print() print('scss') print() print(replacement[2]) path_components = file_path.split('/') file_name = path_components.pop() src_path = '/'.join(path_components) with open(src_path + '/' + file_name, 'r') as component_file_read: component_file = component_file_read.read() component_file_read.close() styled_declaration = "import styled from 'styled-components';" style_decl_with_import = "import styled from 'styled-components';\nimport " + style_file_import + " from './" + style_file_name + "';" component_file = component_file.replace(styled_declaration, style_decl_with_import) for replacement in components_replacement: component_file = component_file.replace(replacement[0], replacement[1]) class_name_attributes = class_name_attribute_no_capture.findall(component_file) for class_name_attribute in class_name_attributes: new_class_name_attribute = transform_class_name_attribute(class_name_attribute, all_styles_class_names_replacement) component_file = component_file.replace(class_name_attribute, new_class_name_attribute) style_file_content = '\n'.join(all_styles) for class_replacement_original, class_replacement_new in all_styles_class_names_replacement.items(): style_file_content = style_file_content.replace('.'+ class_replacement_original, '.'+class_replacement_new) if dry_run: return if file_name.endswith('style.js') or file_name.endswith('index.js') or file_name.endswith('index.jsx') or file_name.endswith('style.jsx'): style_file = src_path + '/' + style_file_name style_file_open = 'w' if os.path.isfile(style_file): style_file_open = 'a' with open(style_file, style_file_open) as style_file: style_file.write(style_file_content) style_file.close() with open(src_path + '/' + file_name, 'w') as component_file_write: component_file_write.write(component_file) component_file_write.close() else: file_dir = file_name.split('.')[0] file_extension = file_name.split('.')[1] new_folder = src_path + '/' + file_dir try: os.mkdir(new_folder) except FileExistsError: pass style_file = new_folder + '/' + style_file_name style_file_open = 'w' if os.path.isfile(style_file): style_file_open = 'a' with open(style_file, style_file_open) as style_file: style_file.write(style_file_content) style_file.close() component_file = component_file.replace("'../", "'../../") component_file = component_file.replace("'./style.module", "'../style.module") component_file = re.sub("'./([A-Z])", "'../\\1", component_file) with open(new_folder + '/index.jsx', 'w') as component_file_write: component_file_write.write(component_file) component_file_write.close() os.remove(file_path) def process_dir(walk_dir='src', verbose=False, dry_run=False): # Iterate through all the files for root, subdirs, files in os.walk(walk_dir): for filename in files: file_path = os.path.join(root, filename) if file_path.endswith(".jsx") or file_path.endswith(".js"): with open(file_path, 'r') as f_in: string = f_in.read() f_in.close() contains_styled = "import styled from" in string styled_components_in_file = [] if contains_styled: if verbose: print(file_path) if "app/pages" in file_path: if verbose: print('>> Requires manual rewrite') continue matches = styled_component_regex_no_capture.findall(string) for match in matches: a = transform_component(match, verbose) if a is not None: styled_components_in_file.append(a) items_to_process_len = len(styled_components_in_file) total_matches_len = len(matches) if verbose: if total_matches_len == 0: print('>> Requires manual rewrite') else: print('>> Total components:', str(items_to_process_len) + '/' + str(total_matches_len)) print() if items_to_process_len != 0: handle_file(file_path, styled_components_in_file, verbose, dry_run) def main(argv): parser = argparse.ArgumentParser(description='Kills styled components.') parser.add_argument('--dir', help='directory to run', default="src") parser.add_argument('--verbose', help='directory to run', action="store_true") parser.add_argument('--dry_run', help='runs without side effects', action="store_true") args = vars(parser.parse_args()) dir = args["dir"] verbose = args["verbose"] dry_run = args["dry_run"] process_dir(dir, verbose, dry_run) if __name__ == "__main__": main(sys.argv[1:])