multi-lang-build 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,431 @@
1
+ """PNPM compiler with mirror acceleration support."""
2
+
3
+ from pathlib import Path
4
+ from typing import Final
5
+ import shutil
6
+ import json
7
+ import os
8
+ import time
9
+
10
+ from multi_lang_build.compiler.base import CompilerBase, BuildResult, CompilerInfo
11
+ from multi_lang_build.mirror.config import get_mirror_config, apply_mirror_environment
12
+
13
+
14
+ class PnpmCompiler(CompilerBase):
15
+ """Compiler for pnpm-based frontend projects."""
16
+
17
+ NAME: Final[str] = "pnpm"
18
+ DEFAULT_MIRROR: Final[str] = "https://registry.npmmirror.com"
19
+
20
+ def __init__(self, pnpm_path: str | None = None) -> None:
21
+ """Initialize the PNPM compiler.
22
+
23
+ Args:
24
+ pnpm_path: Optional path to pnpm executable. If None, uses system PATH.
25
+ """
26
+ self._pnpm_path = pnpm_path
27
+ self._version_cache: str | None = None
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ """Get the compiler name."""
32
+ return self.NAME
33
+
34
+ @property
35
+ def version(self) -> str:
36
+ """Get the pnpm version."""
37
+ if self._version_cache:
38
+ return self._version_cache
39
+
40
+ pnpm_executable = self._get_executable_path()
41
+ import subprocess
42
+
43
+ try:
44
+ result = subprocess.run(
45
+ [pnpm_executable, "--version"],
46
+ capture_output=True,
47
+ text=True,
48
+ timeout=10,
49
+ )
50
+ self._version_cache = result.stdout.strip()
51
+ except Exception:
52
+ self._version_cache = "unknown"
53
+
54
+ return self._version_cache
55
+
56
+ @property
57
+ def supported_mirrors(self) -> list[str]:
58
+ """Get list of supported mirror configurations."""
59
+ return ["npm", "pnpm", "yarn"]
60
+
61
+ def get_info(self) -> CompilerInfo:
62
+ """Get compiler information."""
63
+ return {
64
+ "name": self.name,
65
+ "version": self.version,
66
+ "supported_mirrors": self.supported_mirrors,
67
+ "default_mirror": self.DEFAULT_MIRROR,
68
+ "executable": self._get_executable_path(),
69
+ }
70
+
71
+ def find_project_root(self, start_path: Path) -> Path | None:
72
+ """Find the frontend project root directory containing package.json.
73
+
74
+ Searches from start_path up to its ancestors and also checks subdirectories.
75
+ Returns the first directory containing package.json, or None if not found.
76
+
77
+ Args:
78
+ start_path: Starting path to search from
79
+
80
+ Returns:
81
+ Path to project root or None if not found
82
+ """
83
+ start_path = start_path.resolve()
84
+
85
+ # Check if start_path itself is the root
86
+ if (start_path / "package.json").exists():
87
+ return start_path
88
+
89
+ # Search upwards in the directory tree
90
+ current = start_path.parent
91
+ original_cwd = start_path
92
+ while current != current.parent:
93
+ if (current / "package.json").exists():
94
+ return current
95
+ current = current.parent
96
+
97
+ # Search subdirectories
98
+ for subdir in start_path.iterdir():
99
+ if subdir.is_dir() and not subdir.name.startswith("."):
100
+ subdir_root = self.find_project_root(subdir)
101
+ if subdir_root:
102
+ return subdir_root
103
+
104
+ return None
105
+
106
+ def _execute_in_directory(
107
+ self,
108
+ command: list[str],
109
+ working_dir: Path,
110
+ environment: dict[str, str],
111
+ ) -> BuildResult:
112
+ """Execute a command in the specified directory, returning to original cwd afterwards.
113
+
114
+ Args:
115
+ command: Command to execute
116
+ working_dir: Working directory for the command
117
+ environment: Environment variables
118
+
119
+ Returns:
120
+ BuildResult containing success status and output information
121
+ """
122
+ import subprocess
123
+
124
+ original_cwd = Path.cwd()
125
+ full_command = command.copy()
126
+
127
+ start_time: float = 0.0
128
+
129
+ try:
130
+ os.chdir(working_dir)
131
+
132
+ start_time = time.perf_counter()
133
+
134
+ result = subprocess.run(
135
+ full_command,
136
+ capture_output=True,
137
+ text=True,
138
+ timeout=3600,
139
+ env={**os.environ, **environment},
140
+ )
141
+
142
+ duration = time.perf_counter() - start_time
143
+
144
+ return BuildResult(
145
+ success=result.returncode == 0,
146
+ return_code=result.returncode,
147
+ stdout=result.stdout,
148
+ stderr=result.stderr,
149
+ output_path=working_dir,
150
+ duration_seconds=duration,
151
+ )
152
+
153
+ except subprocess.TimeoutExpired:
154
+ duration = time.perf_counter() - start_time
155
+ return BuildResult(
156
+ success=False,
157
+ return_code=-1,
158
+ stdout="",
159
+ stderr="Build timed out after 1 hour",
160
+ output_path=None,
161
+ duration_seconds=duration,
162
+ )
163
+ except Exception as e:
164
+ duration = time.perf_counter() - start_time
165
+ return BuildResult(
166
+ success=False,
167
+ return_code=-2,
168
+ stdout="",
169
+ stderr=f"Build error: {str(e)}",
170
+ output_path=None,
171
+ duration_seconds=duration,
172
+ )
173
+ finally:
174
+ # Always return to original working directory
175
+ os.chdir(original_cwd)
176
+
177
+ def build(
178
+ self,
179
+ source_dir: Path,
180
+ output_dir: Path,
181
+ *,
182
+ environment: dict[str, str] | None = None,
183
+ mirror_enabled: bool = True,
184
+ extra_args: list[str] | None = None,
185
+ ) -> BuildResult:
186
+ """Execute the pnpm build process with auto-detection of project root.
187
+
188
+ Args:
189
+ source_dir: Source code directory or subdirectory
190
+ output_dir: Build output directory
191
+ environment: Additional environment variables
192
+ mirror_enabled: Whether to use mirror acceleration
193
+ extra_args: Additional arguments to pass to pnpm build
194
+
195
+ Returns:
196
+ BuildResult containing success status and output information.
197
+ """
198
+ pnpm_executable = self._get_executable_path()
199
+
200
+ # Auto-detect project root
201
+ project_root = self.find_project_root(source_dir)
202
+
203
+ if project_root is None:
204
+ return BuildResult(
205
+ success=False,
206
+ return_code=-1,
207
+ stdout="",
208
+ stderr=f"package.json not found in {source_dir} or its parent/sub directories",
209
+ output_path=None,
210
+ duration_seconds=0.0,
211
+ )
212
+
213
+ # Validate directories
214
+ output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
215
+
216
+ # Prepare environment
217
+ env = environment.copy() if environment else {}
218
+
219
+ if mirror_enabled:
220
+ env = apply_mirror_environment("pnpm", env)
221
+ env = apply_mirror_environment("npm", env)
222
+
223
+ # Install dependencies in project root
224
+ install_result = self._execute_in_directory(
225
+ [pnpm_executable, "install"],
226
+ project_root,
227
+ env,
228
+ )
229
+
230
+ if not install_result["success"]:
231
+ return install_result
232
+
233
+ # Run build
234
+ build_args = [pnpm_executable, "build"]
235
+ if extra_args:
236
+ build_args.extend(extra_args)
237
+
238
+ return self._execute_in_directory(
239
+ build_args,
240
+ project_root,
241
+ env,
242
+ )
243
+
244
+ def install_dependencies(
245
+ self,
246
+ source_dir: Path,
247
+ *,
248
+ environment: dict[str, str] | None = None,
249
+ mirror_enabled: bool = True,
250
+ production: bool = False,
251
+ ) -> BuildResult:
252
+ """Install dependencies using pnpm with auto-detection of project root.
253
+
254
+ Args:
255
+ source_dir: Source code directory or subdirectory
256
+ environment: Additional environment variables
257
+ mirror_enabled: Whether to use mirror acceleration
258
+ production: Install only production dependencies
259
+
260
+ Returns:
261
+ BuildResult containing success status and output information.
262
+ """
263
+ pnpm_executable = self._get_executable_path()
264
+
265
+ project_root = self.find_project_root(source_dir)
266
+
267
+ if project_root is None:
268
+ return BuildResult(
269
+ success=False,
270
+ return_code=-1,
271
+ stdout="",
272
+ stderr=f"package.json not found in {source_dir} or its parent/sub directories",
273
+ output_path=None,
274
+ duration_seconds=0.0,
275
+ )
276
+
277
+ env = environment.copy() if environment else {}
278
+
279
+ if mirror_enabled:
280
+ env = apply_mirror_environment("pnpm", env)
281
+
282
+ command = [pnpm_executable, "install"]
283
+ if production:
284
+ command.append("--prod")
285
+
286
+ return self._execute_in_directory(
287
+ command,
288
+ project_root,
289
+ env,
290
+ )
291
+
292
+ def run_script(
293
+ self,
294
+ source_dir: Path,
295
+ script_name: str,
296
+ *,
297
+ environment: dict[str, str] | None = None,
298
+ mirror_enabled: bool = True,
299
+ ) -> BuildResult:
300
+ """Run a specific npm script with auto-detection of project root.
301
+
302
+ Args:
303
+ source_dir: Source code directory or subdirectory
304
+ script_name: Name of the script to run
305
+ environment: Additional environment variables
306
+ mirror_enabled: Whether to use mirror acceleration
307
+
308
+ Returns:
309
+ BuildResult containing success status and output information.
310
+ """
311
+ pnpm_executable = self._get_executable_path()
312
+
313
+ project_root = self.find_project_root(source_dir)
314
+
315
+ if project_root is None:
316
+ return BuildResult(
317
+ success=False,
318
+ return_code=-1,
319
+ stdout="",
320
+ stderr=f"package.json not found in {source_dir} or its parent/sub directories",
321
+ output_path=None,
322
+ duration_seconds=0.0,
323
+ )
324
+
325
+ env = environment.copy() if environment else {}
326
+
327
+ if mirror_enabled:
328
+ env = apply_mirror_environment("pnpm", env)
329
+
330
+ return self._execute_in_directory(
331
+ [pnpm_executable, "run", script_name],
332
+ project_root,
333
+ env,
334
+ )
335
+
336
+ def clean(self, directory: Path) -> bool:
337
+ """Clean pnpm artifacts in the specified directory.
338
+
339
+ Args:
340
+ directory: Directory to clean
341
+
342
+ Returns:
343
+ True if successful, False otherwise.
344
+ """
345
+ import shutil
346
+
347
+ try:
348
+ directory = self._validate_directory(directory, create_if_not_exists=False)
349
+
350
+ # Remove node_modules
351
+ node_modules = directory / "node_modules"
352
+ if node_modules.exists():
353
+ shutil.rmtree(node_modules)
354
+
355
+ # Remove pnpm-lock.yaml
356
+ lock_file = directory / "pnpm-lock.yaml"
357
+ if lock_file.exists():
358
+ lock_file.unlink()
359
+
360
+ # Remove .pnpm-store if exists
361
+ pnpm_store = directory / ".pnpm-store"
362
+ if pnpm_store.exists():
363
+ shutil.rmtree(pnpm_store)
364
+
365
+ return True
366
+
367
+ except Exception:
368
+ return False
369
+
370
+ def _get_executable_path(self) -> str:
371
+ """Get the pnpm executable path."""
372
+ if self._pnpm_path:
373
+ return self._pnpm_path
374
+
375
+ pnpm_path = shutil.which("pnpm")
376
+ if pnpm_path is None:
377
+ raise RuntimeError("pnpm not found in PATH. Please install pnpm or provide pnpm_path.")
378
+
379
+ return pnpm_path
380
+
381
+ @staticmethod
382
+ def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PnpmCompiler":
383
+ """Factory method to create a PnpmCompiler instance.
384
+
385
+ Args:
386
+ source_dir: Source directory for the project
387
+ mirror_enabled: Whether to enable mirror acceleration by default
388
+
389
+ Returns:
390
+ Configured PnpmCompiler instance
391
+ """
392
+ compiler = PnpmCompiler()
393
+ return compiler
394
+
395
+
396
+ def main() -> None:
397
+ """PNPM compiler CLI entry point."""
398
+ import argparse
399
+ import sys
400
+
401
+ parser = argparse.ArgumentParser(description="PNPM Build Compiler")
402
+ parser.add_argument("source_dir", type=Path, help="Source directory")
403
+ parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
404
+ parser.add_argument("--mirror", action="store_true", default=True, help="Enable mirror acceleration")
405
+ parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable mirror acceleration")
406
+ parser.add_argument("--script", type=str, help="Run specific npm script instead of build")
407
+ parser.add_argument("--install", action="store_true", help="Install dependencies only")
408
+
409
+ args = parser.parse_args()
410
+
411
+ compiler = PnpmCompiler()
412
+
413
+ if args.script:
414
+ result = compiler.run_script(
415
+ args.source_dir,
416
+ args.script,
417
+ mirror_enabled=args.mirror,
418
+ )
419
+ elif args.install:
420
+ result = compiler.install_dependencies(
421
+ args.source_dir,
422
+ mirror_enabled=args.mirror,
423
+ )
424
+ else:
425
+ result = compiler.build(
426
+ args.source_dir,
427
+ args.output,
428
+ mirror_enabled=args.mirror,
429
+ )
430
+
431
+ sys.exit(0 if result["success"] else 1)