Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/+init-files-utf8.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`reflex init` now reads and writes `AGENTS.md`, `CLAUDE.md` and `.gitignore` as UTF-8, so it no longer fails or writes them in the Windows ANSI code page on non-UTF-8 locales.
12 changes: 7 additions & 5 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def initialize_gitignore(
# Combine with the current ignored files.
current_ignore: list[str] = []
if gitignore_file.exists():
current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()]
current_ignore = [
ln.strip() for ln in gitignore_file.read_text(encoding="utf-8").splitlines()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Strict UTF-8 decoding of an existing .gitignore now makes reflex init crash on legacy non-UTF-8 files that previously loaded fine. Consider catching UnicodeDecodeError here and reporting a clear warning or error that names the file, instead of letting the exception abort init.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At reflex/utils/frontend_skeleton.py, line 37:

<comment>Strict UTF-8 decoding of an existing .gitignore now makes `reflex init` crash on legacy non-UTF-8 files that previously loaded fine. Consider catching UnicodeDecodeError here and reporting a clear warning or error that names the file, instead of letting the exception abort init.</comment>

<file context>
@@ -33,7 +33,9 @@ def initialize_gitignore(
     if gitignore_file.exists():
-        current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()]
+        current_ignore = [
+            ln.strip() for ln in gitignore_file.read_text(encoding="utf-8").splitlines()
+        ]
 
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing files stop initialization

If an existing .gitignore contains café/ saved as cp1252, this UTF-8 read raises an uncaught UnicodeDecodeError and stops reflex init. Earlier Reflex versions could read and write that file using the Windows locale. The same change affects existing AGENTS.md and CLAUDE.md files.

Keep UTF-8 for writes, but add a fallback for legacy reads and a regression test.

]

if files_to_ignore == current_ignore:
logger.debug(f"{gitignore_file} already up to date.")
Expand All @@ -44,7 +46,7 @@ def initialize_gitignore(
# Write files to the .gitignore file.
gitignore_file.touch(exist_ok=True)
logger.debug(f"Creating {gitignore_file}")
gitignore_file.write_text("\n".join(files_to_ignore) + "\n")
gitignore_file.write_text("\n".join(files_to_ignore) + "\n", encoding="utf-8")


AgentsMdAction = Literal["managed", "bridge"]
Expand Down Expand Up @@ -76,7 +78,7 @@ def _plan_agents_md(
return plan
if (
agents_file.exists() and claude_file.samefile(agents_file)
) or constants.AgentsMd.CLAUDE_IMPORT in claude_file.read_text():
) or constants.AgentsMd.CLAUDE_IMPORT in claude_file.read_text(encoding="utf-8"):
return [(agents_file, "managed")]
if agents_file.exists():
return [(agents_file, "managed"), (claude_file, "managed")]
Expand Down Expand Up @@ -104,7 +106,7 @@ def _apply_agents_md_action(
elif not file.exists():
content = managed_agents_md_text + "\n"
else:
existing = file.read_text()
existing = file.read_text(encoding="utf-8")
begin_idx = existing.find(begin)
end_idx = existing.find(end, begin_idx + len(begin)) if begin_idx != -1 else -1
if end_idx != -1:
Expand All @@ -120,7 +122,7 @@ def _apply_agents_md_action(
f"\n\n{remainder}\n" if remainder else "\n"
)
logger.debug(f"Creating {file}")
file.write_text(content)
file.write_text(content, encoding="utf-8")


def initialize_agents_md(
Expand Down
62 changes: 62 additions & 0 deletions tests/units/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,25 @@ def test_initialize_non_existent_gitignore(
assert set(file_content) - expected == set()


def test_initialize_gitignore_keeps_utf8_entries(tmp_path):
"""Test that an existing .gitignore is read and written as UTF-8 on any locale.

Args:
tmp_path: The root test path.
"""
gitignore_file = tmp_path / ".gitignore"
gitignore_file.write_text("下書き/\n", encoding="utf-8")

frontend_skeleton.initialize_gitignore(
gitignore_file=gitignore_file, files_to_ignore=["*.db"]
)

assert gitignore_file.read_text(encoding="utf-8").splitlines() == [
"*.db",
"下書き/",
]


def test_initialize_agents_md_fetches_canonical(tmp_path, mocker):
"""Test that AGENTS.md is fetched and a CLAUDE.md bridge is created when absent."""
agents_file = tmp_path / "AGENTS.md"
Expand Down Expand Up @@ -596,6 +615,49 @@ def test_initialize_agents_md_handles_symlinked_claude(tmp_path, mocker):
assert claude_file.read_text() == agents_file.read_text()


def test_initialize_agents_md_writes_utf8(tmp_path, mocker):
"""Test that a new AGENTS.md is written as UTF-8 on any locale."""
agents_file = tmp_path / "AGENTS.md"
response = mocker.Mock()
response.text = "Reflex agent skills — 使い方"
mocker.patch("reflex.utils.net.get", return_value=response)

frontend_skeleton.initialize_agents_md(
agents_file=agents_file, claude_file=tmp_path / "CLAUDE.md"
)

assert agents_file.read_text(encoding="utf-8") == (
f"{constants.AgentsMd.BEGIN_MARKER}\n"
"Reflex agent skills — 使い方\n"
f"{constants.AgentsMd.END_MARKER}\n"
)


def test_initialize_agents_md_keeps_utf8_user_content(tmp_path, mocker):
"""Test that existing AGENTS.md and CLAUDE.md files are read as UTF-8 on any locale."""
agents_file = tmp_path / "AGENTS.md"
agents_file.write_text("# 下書きのメモ\n", encoding="utf-8")
claude_file = tmp_path / "CLAUDE.md"
claude_file.write_text("# Claude 用のメモ\n", encoding="utf-8")
response = mocker.Mock()
response.text = "Reflex agent skills — 使い方"
mocker.patch("reflex.utils.net.get", return_value=response)

frontend_skeleton.initialize_agents_md(
agents_file=agents_file, claude_file=claude_file
)

managed = (
f"{constants.AgentsMd.BEGIN_MARKER}\n"
"Reflex agent skills — 使い方\n"
f"{constants.AgentsMd.END_MARKER}"
)
assert agents_file.read_text(encoding="utf-8") == f"{managed}\n\n# 下書きのメモ\n"
assert (
claude_file.read_text(encoding="utf-8") == f"{managed}\n\n# Claude 用のメモ\n"
)


def test_initialize_requirements_txt_skips_when_pyproject_exists(tmp_path):
"""Test that pyproject-based apps do not get a requirements.txt file."""
pyproject_file = tmp_path / "pyproject.toml"
Expand Down