| # Copyright 2021 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 collections |
| import json |
| import os |
| import re |
| import sys |
| import io |
| |
| _CWD = os.getcwd() |
| _HERE_DIR = os.path.dirname(__file__) |
| _SRC_DIR = os.path.normpath(os.path.join(_HERE_DIR, '..', '..')) |
| |
| sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'node')) |
| import node |
| import node_modules |
| |
| from path_mappings import GetDepToPathMappings |
| from validate_tsconfig import validateTsconfigJson, validateJavaScriptAllowed, validateRootDir, isUnsupportedJsTarget, isInAshFolder, isDependencyAllowed, isMappingAllowed, getTargetPath |
| |
| |
| def _write_tsconfig_json(gen_dir, tsconfig, tsconfig_file): |
| if not os.path.exists(gen_dir): |
| os.makedirs(gen_dir) |
| |
| with open(os.path.join(gen_dir, tsconfig_file), 'w', |
| encoding='utf-8') as generated_tsconfig: |
| json.dump(tsconfig, generated_tsconfig, indent=2) |
| return |
| |
| def main(argv): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--raw_deps', nargs='*') |
| parser.add_argument('--deps', nargs='*') |
| parser.add_argument('--gen_dir', required=True) |
| parser.add_argument('--path_mappings', nargs='*') |
| |
| parser.add_argument('--root_gen_dir', required=True) |
| parser.add_argument('--root_src_dir', required=True) |
| |
| parser.add_argument('--root_dir', required=True) |
| parser.add_argument('--out_dir', required=True) |
| parser.add_argument('--tsconfig_base') |
| parser.add_argument('--in_files', nargs='*') |
| parser.add_argument('--manifest_excludes', nargs='*') |
| parser.add_argument('--definitions', nargs='*') |
| parser.add_argument('--composite', action='store_true') |
| parser.add_argument('--platform', |
| choices=['other', 'ios', 'chromeos_ash'], |
| default='other') |
| parser.add_argument('--enable_source_maps', action='store_true') |
| parser.add_argument('--output_suffix', required=True) |
| args = parser.parse_args(argv) |
| |
| root_dir = os.path.relpath(args.root_dir, args.gen_dir) |
| out_dir = os.path.relpath(args.out_dir, args.gen_dir) |
| |
| is_root_dir_valid, error = validateRootDir(args.root_dir, args.gen_dir, |
| args.root_gen_dir, |
| args.platform == 'ios') |
| if not is_root_dir_valid: |
| raise AssertionError(error) |
| |
| TSCONFIG_BASE_PATH = os.path.join(_HERE_DIR, 'tsconfig_base.json') |
| |
| tsconfig = collections.OrderedDict() |
| |
| tsconfig['extends'] = args.tsconfig_base \ |
| if args.tsconfig_base is not None \ |
| else os.path.relpath(TSCONFIG_BASE_PATH, args.gen_dir) |
| |
| tsconfig_base_file = os.path.normpath( |
| os.path.join(args.gen_dir, tsconfig['extends'])) |
| |
| tsconfig['compilerOptions'] = collections.OrderedDict() |
| |
| with io.open(tsconfig_base_file, encoding='utf-8', mode='r') as f: |
| tsconfig_base = json.loads(f.read()) |
| |
| is_base_tsconfig = args.tsconfig_base is None or \ |
| args.tsconfig_base.endswith('/tools/typescript/tsconfig_base.json') |
| is_tsconfig_valid, error = validateTsconfigJson(tsconfig_base, |
| tsconfig_base_file, |
| is_base_tsconfig) |
| if not is_tsconfig_valid: |
| raise AssertionError(error) |
| |
| # Work-around for https://github.com/microsoft/TypeScript/issues/30024. Need |
| # to append 'trusted-types' in cases where the default configuration's |
| # 'types' field is overridden, because of the Chromium patch at |
| # third_party/node/typescript.patch |
| # TODO(dpapad): Remove if/when the TypeScript bug has been fixed. |
| if 'compilerOptions' in tsconfig_base and \ |
| 'types' in tsconfig_base['compilerOptions']: |
| types = tsconfig_base['compilerOptions']['types'] |
| |
| if 'trusted-types' not in types: |
| # Ensure that typeRoots is not overridden in an incompatible way. |
| ERROR_MSG = ('Need to include \'third_party/node/node_modules/@types\' ' |
| 'when overriding the default typeRoots') |
| assert ('typeRoots' in tsconfig_base['compilerOptions']), ERROR_MSG |
| type_roots = tsconfig_base['compilerOptions']['typeRoots'] |
| has_type_root = any(r.endswith('third_party/node/node_modules/@types') \ |
| for r in type_roots) |
| assert has_type_root, ERROR_MSG |
| |
| augmented_types = types.copy() |
| augmented_types.append('trusted-types') |
| tsconfig['compilerOptions']['types'] = augmented_types |
| |
| tsconfig['compilerOptions']['rootDir'] = root_dir |
| tsconfig['compilerOptions']['outDir'] = out_dir |
| |
| includes_js = False |
| if (args.in_files): |
| for file in args.in_files: |
| if file.endswith('.js'): |
| includes_js = True |
| |
| if includes_js or isUnsupportedJsTarget(args.gen_dir, args.root_gen_dir): |
| source_dir = os.path.realpath(os.path.join(_CWD, args.gen_dir, |
| root_dir)).replace('\\', '/') |
| out_dir = os.path.realpath(os.path.join(_CWD, args.gen_dir, |
| out_dir)).replace('\\', '/') |
| is_js_allowed, error = validateJavaScriptAllowed(source_dir, out_dir, |
| args.platform == 'ios') |
| if not is_js_allowed: |
| raise AssertionError(error) |
| tsconfig['compilerOptions']['allowJs'] = True |
| |
| if args.composite: |
| tsbuildinfo_name = f'tsconfig_{args.output_suffix}.tsbuildinfo' |
| tsconfig['compilerOptions']['composite'] = True |
| tsconfig['compilerOptions']['declaration'] = True |
| tsconfig['compilerOptions']['tsBuildInfoFile'] = tsbuildinfo_name |
| |
| if args.enable_source_maps: |
| tsconfig['compilerOptions']['inlineSourceMap'] = True |
| tsconfig['compilerOptions']['inlineSources'] = True |
| tsconfig['compilerOptions']['sourceRoot'] = os.path.realpath( |
| os.path.join(_CWD, args.gen_dir, root_dir)) |
| |
| tsconfig['files'] = [] |
| if args.in_files is not None: |
| # Source .ts files are always resolved as being relative to |root_dir|. |
| tsconfig['files'].extend([os.path.join(root_dir, f) for f in args.in_files]) |
| |
| if args.definitions is not None: |
| for d in args.definitions: |
| assert d.endswith( |
| '.d.ts'), f'Invalid definition \'{d}\'. Should end with \'.d.ts\'' |
| tsconfig['files'].extend(args.definitions) |
| |
| # Handle path mappings, for example chrome://resources/ URLs. |
| path_mappings = collections.defaultdict(list) |
| |
| target_path = getTargetPath(args.gen_dir, args.root_gen_dir) |
| is_ash_target = isInAshFolder(target_path) |
| |
| if args.deps is not None: |
| tsconfig['references'] = [{'path': dep} for dep in args.deps] |
| |
| assert args.raw_deps is not None |
| dep_to_path_mappings = GetDepToPathMappings( |
| args.root_gen_dir, |
| # Sometimes root_src_dir has trailing slashes. Remove them if necessary. |
| args.root_src_dir.rstrip('/'), |
| args.platform) |
| |
| for dep in args.raw_deps: |
| dependencyType = 'Browser-only' if is_ash_target else 'Ash-only' |
| assert isDependencyAllowed(is_ash_target, dep, target_path), \ |
| f'{target_path} should not use {dependencyType} dependency {dep}.' |
| |
| if dep not in dep_to_path_mappings: |
| assert not dep.startswith("//ui/webui/resources"), \ |
| f'Missing path mapping for \'{dep}\'. Update ' \ |
| '//tools/typescript/path_mappings.py accordingly.' |
| |
| # Path mappings outside of //ui/webui/resources are not inferred from |
| # |args.deps| yet. |
| continue |
| |
| mappings = dep_to_path_mappings[dep] |
| for (url, dir) in mappings: |
| path_mappings[url].append(os.path.join('./', dir)) |
| path_mappings['chrome:' + url].append(os.path.join('./', dir)) |
| |
| if args.path_mappings is not None: |
| for m in args.path_mappings: |
| mapping = m.split('|') |
| mapping_path = os.path.relpath(mapping[1], args.root_src_dir) |
| assert isMappingAllowed(is_ash_target, target_path, mapping_path), \ |
| f'Cannot use mapping to Ash-specific folder {mapping_path} from ' \ |
| f'non-Ash target {target_path}' |
| path_mappings[mapping[0]].append(os.path.join('./', mapping[1])) |
| |
| tsconfig['compilerOptions']['paths'] = path_mappings |
| |
| tsconfig_file = f'tsconfig_{args.output_suffix}.json' |
| _write_tsconfig_json(args.gen_dir, tsconfig, tsconfig_file) |
| |
| # Detect and delete obsolete files that can cause build problems. |
| if args.in_files is not None: |
| for f in args.in_files: |
| [pathname, extension] = os.path.splitext(f) |
| |
| # Delete any obsolete .ts files (from previous builds) corresponding to |
| # .js |in_files| in the |root_dir| folder, as they would cause the |
| # following error to be thrown: |
| # |
| # "error TS5056: Cannot write file '...' because it would be overwritten |
| # by multiple input files." |
| # |
| # This can happen when a ts_library() is migrating JS to TS one file at a |
| # time and a bot is switched from building a later CL to building an |
| # earlier CL. |
| if extension == '.js': |
| to_check = os.path.join(args.root_dir, pathname + '.ts') |
| if os.path.exists(to_check): |
| os.remove(to_check) |
| |
| # Delete any obsolete .d.ts files (from previous builds) corresponding to |
| # .ts |in_files| in |root_dir| folder. |
| # |
| # This can happen when a ts_library() is migrating JS to TS one file at a |
| # time and a previous checked-in or auto-generated .d.ts file is now |
| # obsolete. |
| if extension == '.ts': |
| to_check = os.path.join(args.root_dir, pathname + '.d.ts') |
| if os.path.exists(to_check): |
| os.remove(to_check) |
| |
| # Delete any obsolete .ts files (from previous builds) corresponding to |
| # .ts |in_files| in |out_dir| folder, only done when |root_dir| and |
| # |out_dir| are different folders. |
| if args.root_dir != args.out_dir: |
| to_check = os.path.join(args.out_dir, f) |
| if os.path.exists(to_check): |
| os.remove(to_check) |
| |
| try: |
| node.RunNode([ |
| node_modules.PathToTypescript(), '--project', |
| os.path.join(args.gen_dir, tsconfig_file) |
| ]) |
| finally: |
| if args.composite: |
| # `.tsbuildinfo` is generated by TypeScript for incremenetal compilation |
| # freshness checks. Since GN already decides which ts_library() targets |
| # are dirty, `.tsbuildinfo` is not needed for our purposes and is |
| # deleted. |
| # |
| # Moreover `.tsbuildinfo` can cause flakily failing builds since the TS |
| # compiler checks the `.tsbuildinfo` file and sees that none of the |
| # source files are changed and does not regenerate any output, without |
| # checking whether output files have been modified/deleted, which can |
| # lead to bad builds (missing files or picking up obsolete generated |
| # files). |
| tsbuildinfo_path = os.path.join(args.gen_dir, tsbuildinfo_name) |
| if os.path.exists(tsbuildinfo_path): |
| os.remove(tsbuildinfo_path) |
| |
| if args.in_files is not None: |
| |
| manifest_path = os.path.join(args.gen_dir, |
| f'{args.output_suffix}_manifest.json') |
| with open(manifest_path, 'w', encoding='utf-8') as manifest_file: |
| manifest_data = {} |
| manifest_data['base_dir'] = args.out_dir |
| manifest_files = args.in_files |
| if args.manifest_excludes is not None: |
| manifest_files = filter(lambda f: f not in args.manifest_excludes, |
| args.in_files) |
| manifest_data['files'] = \ |
| [re.sub(r'\.ts$', '.js', f) for f in manifest_files] |
| json.dump(manifest_data, manifest_file) |
| |
| |
| if __name__ == '__main__': |
| main(sys.argv[1:]) |