1818from pathlib import Path
1919
2020THIS_DIRECTORY = Path (__file__ ).parent .absolute ()
21- EXAMPLE_DIRECTORIES = [d for d in (THIS_DIRECTORY / "examples" ).iterdir () if d .is_dir ()]
21+ VERSIONS = ["v1" , "v2" ]
22+ EXAMPLE_DIRECTORIES = [
23+ d
24+ for v in VERSIONS
25+ if (THIS_DIRECTORY / "examples" / v ).is_dir ()
26+ for d in (THIS_DIRECTORY / "examples" / v ).iterdir ()
27+ if d .is_dir ()
28+ ]
2229TEMPLATE_DIRECTORIES = [
23- THIS_DIRECTORY / "template" ,
24- THIS_DIRECTORY / "template-reactless" ,
30+ d
31+ for v in VERSIONS
32+ if (THIS_DIRECTORY / "templates" / v ).is_dir ()
33+ for d in (THIS_DIRECTORY / "templates" / v ).iterdir ()
34+ if d .is_dir ()
2535]
2636
2737
@@ -35,6 +45,97 @@ def run_verbose(cmd_args, *args, **kwargs):
3545 subprocess .run (cmd_args , * args , ** kwargs )
3646
3747
48+ def create_sanitized_copies_excluding_gitignored (
49+ source_dir_a : Path ,
50+ source_dir_b : Path ,
51+ repo_root : Path ,
52+ destination_base_dir : Path ,
53+ ) -> typing .Tuple [Path , Path ]:
54+ """
55+ Create sanitized copies of two directories while excluding files ignored by Git.
56+
57+ This function is intended for robust, cross-platform directory comparisons (e.g.,
58+ git diff --no-index) where we want to ignore anything matched by the repository's
59+ ignore rules (.gitignore, global and excludesfile), but still compare the remaining
60+ content 1:1.
61+
62+ Rationale:
63+ - git diff --no-index does not honor .gitignore rules directly for arbitrary
64+ directory arguments.
65+ - Filtering via Git pathspecs is not supported with --no-index in a portable way.
66+ - By asking Git for the ignored files under source_dir_b (the path inside the
67+ repo) and excluding those relative paths from BOTH copies, we avoid false
68+ positives from OS metadata and any user-ignored files.
69+
70+ Parameters:
71+ - source_dir_a: First directory to compare (e.g., freshly rendered output).
72+ - source_dir_b: Second directory to compare (must be within repo_root).
73+ - repo_root: Root of the Git repository (used to query ignored files).
74+ - destination_base_dir: Directory where sanitized copies will be created.
75+
76+ Returns:
77+ - Tuple[Path, Path]: (sanitized_copy_of_a, sanitized_copy_of_b)
78+ """
79+ # Compute the list of ignored files under source_dir_b using Git
80+ try :
81+ rel_path_in_repo = source_dir_b .relative_to (repo_root )
82+ ls_cmd = [
83+ "git" ,
84+ "-C" ,
85+ str (repo_root ),
86+ "ls-files" ,
87+ "-i" , # list ignored files
88+ "--exclude-standard" , # honor .gitignore, global ignores, etc.
89+ "--others" , # show untracked files as well
90+ "-z" , # null-terminate paths to be robust to special characters
91+ "--" ,
92+ str (rel_path_in_repo ),
93+ ]
94+ result = subprocess .run (ls_cmd , check = True , stdout = subprocess .PIPE , text = False )
95+ ignored_absolute_paths = [
96+ repo_root / p .decode () for p in result .stdout .split (b"\0 " ) if p
97+ ]
98+ except (ValueError , subprocess .CalledProcessError ):
99+ # ValueError if source_dir_b is not under repo_root; fallback to no ignores
100+ ignored_absolute_paths = []
101+
102+ ignored_relative_to_b = set (
103+ str (p .relative_to (source_dir_b ).as_posix ())
104+ for p in ignored_absolute_paths
105+ if hasattr (p , "is_relative_to" )
106+ and p .is_relative_to (source_dir_b )
107+ or (str (p ).startswith (str (source_dir_b ) + os .sep ))
108+ )
109+
110+ def build_ignore_filter (source_root : Path ):
111+ def _ignore (dirpath : str , names : typing .List [str ]) -> typing .List [str ]:
112+ rel_dir = Path (dirpath ).relative_to (source_root )
113+ to_ignore : typing .List [str ] = []
114+ for name in names :
115+ candidate = (rel_dir / name ).as_posix ()
116+ if candidate in ignored_relative_to_b :
117+ to_ignore .append (name )
118+ return to_ignore
119+
120+ return _ignore
121+
122+ sanitized_a = destination_base_dir / "sanitized-output"
123+ sanitized_b = destination_base_dir / "sanitized-repo"
124+
125+ shutil .copytree (
126+ source_dir_a ,
127+ sanitized_a ,
128+ ignore = build_ignore_filter (source_dir_a ),
129+ )
130+ shutil .copytree (
131+ source_dir_b ,
132+ sanitized_b ,
133+ ignore = build_ignore_filter (source_dir_b ),
134+ )
135+
136+ return sanitized_a , sanitized_b
137+
138+
38139# Commands
39140def cmd_all_npm_install (args ):
40141 """Install all node dependencies for all examples"""
@@ -134,10 +235,17 @@ def cmd_all_python_build_package(args):
134235 final_dist_directory = THIS_DIRECTORY / "dist"
135236 final_dist_directory .mkdir (exist_ok = True )
136237 for project_dir in EXAMPLE_DIRECTORIES + TEMPLATE_DIRECTORIES :
137- run_verbose (
138- [sys .executable , "setup.py" , "bdist_wheel" , "--universal" , "sdist" ],
139- cwd = str (project_dir ),
140- )
238+ pyproject_file = project_dir / "pyproject.toml"
239+ if pyproject_file .exists ():
240+ run_verbose (
241+ [sys .executable , "-m" , "build" , "--wheel" , "--sdist" ],
242+ cwd = str (project_dir ),
243+ )
244+ else :
245+ run_verbose (
246+ [sys .executable , "setup.py" , "bdist_wheel" , "--universal" , "sdist" ],
247+ cwd = str (project_dir ),
248+ )
141249
142250 wheel_file = next (project_dir .glob ("dist/*.whl" ))
143251 shutil .copy (wheel_file , final_dist_directory )
@@ -172,7 +280,13 @@ def cmd_example_check_deps(args):
172280 """Checks that dependencies of examples match the template"""
173281 template_deps = json .loads (
174282 (
175- THIS_DIRECTORY / "template" / "my_component" / "frontend" / "package.json"
283+ THIS_DIRECTORY
284+ / "templates"
285+ / "v1"
286+ / "template"
287+ / "my_component"
288+ / "frontend"
289+ / "package.json"
176290 ).read_text ()
177291 )
178292 examples_package_jsons = sorted (
@@ -220,19 +334,22 @@ def cmd_check_test_utils(args):
220334class CookiecutterVariant (typing .NamedTuple ):
221335 replay_file : Path
222336 repo_directory : Path
337+ cookiecutter_dir : Path
223338
224339
225340COOKIECUTTER_VARIANTS = [
226341 CookiecutterVariant (
227342 replay_file = THIS_DIRECTORY / ".github" / "replay-files" / "template.json" ,
228- repo_directory = THIS_DIRECTORY / "template" ,
343+ repo_directory = THIS_DIRECTORY / "templates" / "v1" / "template" ,
344+ cookiecutter_dir = THIS_DIRECTORY / "cookiecutter" / "v1" ,
229345 ),
230346 CookiecutterVariant (
231347 replay_file = THIS_DIRECTORY
232348 / ".github"
233349 / "replay-files"
234350 / "template-reactless.json" ,
235- repo_directory = THIS_DIRECTORY / "template-reactless" ,
351+ repo_directory = THIS_DIRECTORY / "templates" / "v1" / "template-reactless" ,
352+ cookiecutter_dir = THIS_DIRECTORY / "cookiecutter" / "v1" ,
236353 ),
237354]
238355
@@ -256,7 +373,7 @@ def cmd_check_templates_using_cookiecutter(args):
256373 str (cookiecutter_variant .replay_file ),
257374 "--output-dir" ,
258375 str (output_dir ),
259- str (THIS_DIRECTORY / "cookiecutter" / "v1" ),
376+ str (cookiecutter_variant . cookiecutter_dir ),
260377 ]
261378 )
262379 try :
@@ -267,14 +384,23 @@ def cmd_check_templates_using_cookiecutter(args):
267384 Path (output_dir )
268385 / replay_file_content ["cookiecutter" ]["package_name" ]
269386 )
387+ # Create sanitized copies excluding all files ignored by git in the repo
388+ sanitized_output , sanitized_repo = (
389+ create_sanitized_copies_excluding_gitignored (
390+ output_template ,
391+ cookiecutter_variant .repo_directory ,
392+ THIS_DIRECTORY ,
393+ Path (output_dir ),
394+ )
395+ )
270396 run_verbose (
271397 [
272398 "git" ,
273399 "--no-pager" ,
274400 "diff" ,
275401 "--no-index" ,
276- str (output_template ),
277- str (cookiecutter_variant . repo_directory ),
402+ str (sanitized_output ),
403+ str (sanitized_repo ),
278404 ]
279405 )
280406 except subprocess .CalledProcessError :
@@ -311,7 +437,7 @@ def cmd_update_templates(args):
311437 str (cookiecutter_variant .replay_file ),
312438 "--output-dir" ,
313439 str (output_dir ),
314- str (THIS_DIRECTORY / "cookiecutter" / "v1" ),
440+ str (cookiecutter_variant . cookiecutter_dir ),
315441 ]
316442 )
317443 print (
0 commit comments