chromium/ash/webui/recorder_app_ui/resources/scripts/cra/commands/bundle.py

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

import logging
import os
import pathlib
import queue
import re
import shutil
from typing import Optional, TypeVar

from cra import build
from cra import cli
from cra import util
from cra.commands import dev as dev_cmd

_BUNDLE_TSC_OUTPUT_TEMP_DIR = pathlib.Path("/tmp/cra-bundle-out")

T = TypeVar('T')

_RequestPath = pathlib.PurePosixPath


def _assert_exist(val: Optional[T]) -> T:
    assert val is not None
    return val


def _get_static_paths(cra_root: pathlib.Path) -> set[_RequestPath]:
    static_paths = set([
        _RequestPath('index.html'),
        _RequestPath("chrome_stub/theme/typography.css"),
        _RequestPath("chrome_stub/theme/colors.css"),
    ])
    static_root = cra_root / "static"

    # Path.walk() is only available in Python 3.12, and glinux default system
    # Python is at 3.11.
    # TODO(pihsun): Change to Path.walk when newer Python is available by
    # default.
    for dirpath, _, filenames in os.walk(static_root):
        dirpath = pathlib.Path(dirpath)
        relative_dir = dirpath.relative_to(static_root)
        static_base = _RequestPath(static_root.name)
        for file in filenames:
            if file.endswith(".gni"):
                continue
            static_paths.add(static_base / relative_dir / file)

    return static_paths


@cli.command(
    "bundle",
    help="bundle cra",
    description="bundle cra as a static website that can be served by any "
    "static HTTP file server with SPA support",
)
@util.build_dir_option()
def cmd(build_dir: pathlib.Path) -> int:
    _BUNDLE_TSC_OUTPUT_TEMP_DIR.mkdir(parents=True, exist_ok=True)

    build.generate_tsconfig(build_dir)

    cra_root = util.get_cra_root()

    util.run_node(
        [
            "typescript/bin/tsc",
            "--outDir",
            str(_BUNDLE_TSC_OUTPUT_TEMP_DIR),
            "--noEmit",
            "false",
            # Makes compilation faster
            "--incremental",
            # For better debugging experience.
            "--inlineSourceMap",
            "--inlineSources",
            # Makes devtools show TypeScript source with better path
            "--sourceRoot",
            "/",
            # For easier developing / test cycle.
            "--noUnusedLocals",
            "false",
            "--noUnusedParameters",
            "false",
        ],
        cwd=cra_root)

    handler = dev_cmd.RequestHandler(cra_root, _BUNDLE_TSC_OUTPUT_TEMP_DIR,
                                     build_dir, util.get_strings_dir())

    output_folder = cra_root / "dist"
    shutil.rmtree(output_folder, ignore_errors=True)
    output_folder.mkdir(parents=True, exist_ok=True)

    # Gets the response from `path` and write the response to output bundle
    # folder.
    def handle(path: _RequestPath) -> bytes:
        if path.is_absolute():
            path = path.relative_to(_RequestPath("/"))
        logging.debug(f'Processing {path}')
        response = _assert_exist(handler.handle(path))[0]
        output_path = output_folder / path
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, "wb") as fout:
            fout.write(response)
        return response

    for static_path in _get_static_paths(cra_root):
        handle(static_path)

    js_paths = queue.Queue[_RequestPath]()
    added_js_paths = set[_RequestPath]()

    def add_path(path: _RequestPath):
        if path in added_js_paths:
            return
        js_paths.put(path)
        added_js_paths.add(path)

    add_path(_RequestPath("init.js"))

    while not js_paths.empty():
        path = js_paths.get_nowait()
        dir = path.parent
        response = handle(path).decode()
        for match in re.finditer(r'^import\s+(?:[^;]*?)([\'"])([^\'"]*?)\1',
                                 response, re.MULTILINE | re.DOTALL):
            # Why is there no normpath in pathlib... T_T
            import_path = _RequestPath(os.path.normpath(dir / match[2]))
            add_path(import_path)

    return 0