|
| 1 | +# 편의상 "next data"라고 부르나 정식 명칭은 RSC payload임. |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
1 | 5 | import json |
2 | | -from operator import attrgetter |
3 | 6 | import re |
4 | 7 | import typing |
5 | 8 |
|
| 9 | +from ._base import logger |
| 10 | + |
6 | 11 | next_f_data = re.compile(r"self\.__next_f\.push\(\[\d+,\s*(.*)\]\)", re.DOTALL) |
| 12 | +# HL, I, "$"가 각각 어떤 역할을 하는지 알려면 https://roy-jung.github.io/250323-react-server-components/ 이 코드 참고 |
7 | 13 | line_regex = re.compile(r"^\s*(?P<hexdigit>[0-9a-fA-F]+):(?P<data_prefix>[A-Z]*)(?P<data_raw>.*)") |
8 | 14 |
|
9 | 15 |
|
10 | 16 | class NextData(typing.NamedTuple): |
11 | | - script_no: int |
12 | 17 | line_no: int |
13 | 18 | hexdigit: str |
14 | 19 | prefix: str |
15 | 20 | value: typing.Any |
| 21 | + parsed: bool |
16 | 22 |
|
17 | 23 |
|
18 | | -def extract_next_data(scripts: typing.Iterable[str], prefix_to_ignore: typing.Container[str] | None = None) -> list[NextData]: |
| 24 | +def extract_next_data(scripts: typing.Iterable[str], prefix_to_ignore: typing.Container[str] | None = None, warn_not_parsed: bool = False) -> list[NextData]: |
19 | 25 | line: str |
20 | | - to_be_continued: str | None = None |
21 | 26 | next_data = [] |
22 | | - for script_no, script in enumerate(scripts): |
| 27 | + joined = "" |
| 28 | + for script in scripts: |
23 | 29 | matched = next_f_data.match(script) |
24 | 30 | if not matched: |
25 | | - # assert "self.__next_f.push(1" not in script, script |
26 | 31 | continue |
27 | | - for line_no, line in enumerate(json.loads(matched[1]).split("\n")): |
28 | | - if not line: |
29 | | - continue |
30 | | - matched = line_regex.match(line) |
31 | | - if not matched: |
32 | | - if to_be_continued is None: |
33 | | - raise ValueError(f"Line {line_no} in script {script_no} does not match the expected format: {line!r}") |
34 | | - |
35 | | - data_raw = to_be_continued + line |
36 | | - to_be_continued = None |
37 | | - if prefix_to_ignore and data_prefix in prefix_to_ignore: # noqa: F821 |
38 | | - continue |
39 | | - # script_no와 line_no 데이터는 continuation의 데이터가 사용되고, |
40 | | - # hexdigit과 data_prefix는 이전 matched의 데이터를 사용 |
41 | | - # 가능하면 script_no와 line_no 데이터도 이전 matched의 데이터를 사용하면 좋지만, |
42 | | - # 굳이 중요한 건 아니니 이렇게 구현함 |
43 | | - next_data.append(NextData(script_no, line_no, hexdigit, data_prefix, json.loads(data_raw))) # noqa: F821 |
44 | | - continue |
45 | | - elif to_be_continued is not None: |
46 | | - raise ValueError(f"Line {line_no} in script {script_no} does not match the expected format: {line!r}") |
47 | | - |
48 | | - hexdigit = matched["hexdigit"] |
49 | | - data_prefix = matched["data_prefix"] |
50 | | - data_raw = matched["data_raw"] |
51 | | - try: |
52 | | - json_data = json.loads(data_raw) |
53 | | - except json.JSONDecodeError: |
54 | | - to_be_continued = data_raw |
55 | | - continue |
56 | | - if prefix_to_ignore and data_prefix in prefix_to_ignore: |
57 | | - continue |
58 | | - next_data.append(NextData(script_no, line_no, hexdigit, data_prefix, json_data)) |
| 32 | + joined += json.loads(matched[1]) |
| 33 | + |
| 34 | + for line_no, line in enumerate(joined.split("\n")): |
| 35 | + if not line: |
| 36 | + continue |
| 37 | + matched = line_regex.match(line) |
| 38 | + if not matched: |
| 39 | + raise ValueError(f"Line {line_no} does not match the expected format: {line!r}") |
| 40 | + |
| 41 | + hexdigit = matched["hexdigit"] |
| 42 | + data_prefix = matched["data_prefix"] |
| 43 | + data_raw = matched["data_raw"] |
| 44 | + if prefix_to_ignore and data_prefix in prefix_to_ignore: |
| 45 | + continue |
| 46 | + try: |
| 47 | + json_data = json.loads(data_raw) |
| 48 | + except json.JSONDecodeError: |
| 49 | + if warn_not_parsed: |
| 50 | + logger.warning(f"Failed to parse following data to JSON: {data_raw}") |
| 51 | + json_data = data_raw |
| 52 | + parsed = False |
| 53 | + else: |
| 54 | + parsed = True |
| 55 | + next_data.append(NextData(line_no, hexdigit, data_prefix, json_data, parsed)) |
59 | 56 |
|
60 | 57 | next_data.sort(key=lambda x: int(x.hexdigit, 16)) |
61 | 58 | return next_data |
0 commit comments