Created
June 28, 2019 12:00
-
-
Save tommyip/c113beb04295fd1225dabd3e789f0445 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
1. Convert CommonJS to typescript module | |
1.1 Remove IIFE wrapping | |
1.2 Use ES6 style import | |
1.3 Use ES6 style export | |
1.4 Import from other modules | |
1.5 Remove from .eslintrc | |
1.6 Remove from js_typings | |
1.7 Remove from app.js bundle | |
1.8 Modifiy entry in test-js-with-node with .ts extension | |
1.9 Search codebase for mention of <module>.js | |
2. Add typings (use dts-gen?) | |
3. var -> let/const | |
4. Use typescript/es6 stuff | |
""" | |
import argparse | |
import os | |
import re | |
# from subprocess import call, check_output | |
from typing import Text, AnyStr | |
STATIC_JS_PATH = 'static/js/' | |
# Map name to module path | |
THIRD_PARTY_FUNCTION = { | |
'md5': 'blueimp-md5', | |
'ClipboardJS': 'clipboard', | |
'XDate': 'xdate', | |
'$': 'jquery', | |
'jQuery': 'jquery', | |
'toMarkdown': 'to-markdown', | |
} | |
THIRD_PARTY_NAMESPACE = { | |
'marked': '../third/marked/lib/marked.js', | |
'emoji_codes': '../generated/emoji/emoji_codes.js', | |
'pygments_data': '../generated/pygments_data.js', | |
'$': 'jquery', | |
'jQuery': 'jquery', | |
'_': 'underscore', | |
'Handlebars': 'handlebars/runtime', | |
'Sortable': 'sortablejs', | |
'WinChan': 'winchan', | |
} | |
def _remove_iife(ts_content: Text, module: Text) -> Text: | |
ts_content = ts_content.replace(f'var {module} = (function () {{\n\nvar exports = {{}};\n\n', '') | |
ts_content = re.sub(f'return exports;.+window\\.{module} = {module};\n', '', ts_content, flags=re.DOTALL) | |
return ts_content.strip() + '\n' | |
def _es6_imports(ts_content: Text) -> Text: | |
""" Check whether the module uses any third party libraries and import | |
them accordingly """ | |
third_party_functions = THIRD_PARTY_FUNCTION.keys() | |
third_party_namespace = THIRD_PARTY_NAMESPACE.keys() | |
combined = {**THIRD_PARTY_FUNCTION, **THIRD_PARTY_NAMESPACE} | |
# Dict is already imported but with require | |
ts_content = ts_content.replace( | |
"var Dict = require('./dict').Dict;", | |
"import { Dict } from './dict';") | |
function_match = re.findall(rf'({"|".join(third_party_functions)})\(', ts_content) | |
namespace_match = re.findall(rf'({"|".join(third_party_namespace)})\.', ts_content) | |
unique_matches = list(set(function_match + namespace_match)) | |
imports = [] | |
if unique_matches: | |
for library in unique_matches: | |
imports.append(f"import {library} from '{combined[library]}';") | |
ts_content = '\n'.join(imports) + '\n' + ts_content | |
return ts_content | |
def _es6_exports(ts_content: Text) -> Text: | |
""" Use es6's exports when declaring public functions """ | |
ts_content = re.sub( | |
'exports\\.([a-z_]+) = function (?:[a-z_]+)?\\((.*?)\\) {', | |
'export function \\g<1>(\\g<2>) {', | |
ts_content) | |
ts_content = re.sub('\n};', '\n}', ts_content) | |
ts_content = ts_content.replace('exports.', '') | |
return ts_content | |
def _require_from_other_js_modules(module: Text) -> None: | |
for filename in os.listdir(STATIC_JS_PATH): | |
if filename.endswith('.js'): | |
with open(STATIC_JS_PATH + filename) as f: | |
content = f.read() | |
if module + '.' in content: | |
require = f"var {module} = require('./{module}');" | |
# Check whether there are existing requires and insert at | |
# appropriate position by alphabetical order | |
lines = content.split('\n') | |
for insert_idx, line in enumerate(lines): | |
if 'require' not in line: | |
lines.insert(insert_idx, require) | |
if insert_idx == 0: | |
lines.insert(insert_idx + 1, '') | |
break | |
elif require < line: | |
lines.insert(insert_idx, require) | |
break | |
with open(STATIC_JS_PATH + filename, 'w') as f: | |
f.write('\n'.join(lines)) | |
def _replace_in_file(pattern: AnyStr, replacement: AnyStr, filepath: str) -> None: | |
with open(filepath) as f: | |
content = f.read() | |
with open(filepath, 'w') as wf: | |
wf.write(re.sub(pattern, replacement, content)) | |
def convert_module_system(module: Text) -> None: | |
js = STATIC_JS_PATH + module + '.js' | |
ts = STATIC_JS_PATH + module + '.ts' | |
with open(js) as f: | |
js_content = f.read() | |
ts_content = js_content | |
ts_content = _remove_iife(ts_content, module) | |
ts_content = _es6_imports(ts_content) | |
ts_content = _es6_exports(ts_content) | |
_require_from_other_js_modules(module) | |
_replace_in_file(f' +\"{module}\": false,\n', '', '.eslintrc.json') | |
_replace_in_file(f'declare var {module}: any;\n', '', STATIC_JS_PATH + 'js_typings/zulip/index.d.ts') | |
_replace_in_file(f'import \"js/{module}.js\";\n', '', STATIC_JS_PATH + 'bundles/app.js') | |
_replace_in_file(f'static/js/{module}\.js', f'static/js/{module}.ts', 'tools/test-js-with-node') | |
with open(ts, 'w') as f: | |
f.write(ts_content) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
parser.add_argument('module', type=str, help='JS module to be converted to TS') | |
group = parser.add_mutually_exclusive_group(required=True) | |
group.add_argument('--convert-module-system', action='store_true') | |
group.add_argument('--add-types', action='store_true') | |
args = parser.parse_args() | |
# Check this module exists | |
if not os.path.isfile(STATIC_JS_PATH + args.module + '.js'): | |
raise Exception('Cannot find module ' + args.module) | |
if args.convert_module_system: | |
convert_module_system(args.module) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment