plain.code 0.20.2__tar.gz → 0.21.0__tar.gz
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.
- {plain_code-0.20.2 → plain_code-0.21.0}/PKG-INFO +15 -14
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/CHANGELOG.md +24 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/README.md +14 -13
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/agents/.claude/rules/plain-code.md +2 -2
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/cli.py +42 -34
- plain_code-0.21.0/plain/code/oxc.py +202 -0
- plain_code-0.21.0/plain/code/oxfmt_defaults.json +10 -0
- plain_code-0.21.0/plain/code/oxlint_defaults.json +10 -0
- plain_code-0.21.0/plain/code/ruff_defaults.toml +23 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/pyproject.toml +3 -9
- plain_code-0.20.2/plain/code/biome.py +0 -162
- plain_code-0.20.2/plain/code/biome_defaults.json +0 -23
- plain_code-0.20.2/plain/code/ruff_defaults.toml +0 -23
- {plain_code-0.20.2 → plain_code-0.21.0}/.gitignore +0 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/LICENSE +0 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/README.md +0 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/__init__.py +0 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/annotations.py +0 -0
- {plain_code-0.20.2 → plain_code-0.21.0}/plain/code/entrypoints.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Preconfigured code formatting and linting.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -32,9 +32,10 @@ Plain.code provides comprehensive code quality tools with sensible defaults:
|
|
|
32
32
|
|
|
33
33
|
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
34
34
|
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
35
|
-
- **[
|
|
35
|
+
- **[oxlint](https://oxc.rs/)** - JavaScript and TypeScript linting
|
|
36
|
+
- **[oxfmt](https://oxc.rs/)** - JavaScript, TypeScript, JSON, and CSS formatting
|
|
36
37
|
|
|
37
|
-
Ruff and ty are installed as Python dependencies.
|
|
38
|
+
Ruff and ty are installed as Python dependencies. oxlint and oxfmt are managed automatically as standalone binaries (npm is not required).
|
|
38
39
|
|
|
39
40
|
## Commands
|
|
40
41
|
|
|
@@ -73,10 +74,10 @@ You can skip specific tools if needed:
|
|
|
73
74
|
plain code check --skip-ty
|
|
74
75
|
|
|
75
76
|
# Only run type checks
|
|
76
|
-
plain code check --skip-ruff --skip-
|
|
77
|
+
plain code check --skip-ruff --skip-oxc
|
|
77
78
|
|
|
78
|
-
# Skip
|
|
79
|
-
plain code check --skip-
|
|
79
|
+
# Skip oxlint and oxfmt checks
|
|
80
|
+
plain code check --skip-oxc
|
|
80
81
|
|
|
81
82
|
# Skip annotation coverage checks
|
|
82
83
|
plain code check --skip-annotations
|
|
@@ -108,7 +109,7 @@ plain code annotations --json
|
|
|
108
109
|
|
|
109
110
|
## Settings
|
|
110
111
|
|
|
111
|
-
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml)
|
|
112
|
+
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml).
|
|
112
113
|
|
|
113
114
|
You can customize the behavior in your `pyproject.toml`:
|
|
114
115
|
|
|
@@ -119,9 +120,9 @@ exclude = ["path/to/exclude"]
|
|
|
119
120
|
[tool.plain.code.ty]
|
|
120
121
|
enabled = true # Set to false to disable ty
|
|
121
122
|
|
|
122
|
-
[tool.plain.code.
|
|
123
|
-
enabled = true # Set to false to disable
|
|
124
|
-
version = "1.
|
|
123
|
+
[tool.plain.code.oxc]
|
|
124
|
+
enabled = true # Set to false to disable oxlint/oxfmt
|
|
125
|
+
version = "1.43.0" # Pin to a specific version
|
|
125
126
|
|
|
126
127
|
[tool.plain.code.annotations]
|
|
127
128
|
enabled = true # Set to false to disable annotation checks
|
|
@@ -130,16 +131,16 @@ exclude = ["migrations"] # Exclude specific patterns
|
|
|
130
131
|
|
|
131
132
|
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
132
133
|
|
|
133
|
-
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty,
|
|
134
|
+
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, oxlint, oxfmt) directly instead.
|
|
134
135
|
|
|
135
136
|
## FAQs
|
|
136
137
|
|
|
137
|
-
#### How do I install or update
|
|
138
|
+
#### How do I install or update oxlint/oxfmt manually?
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
oxlint and oxfmt are installed automatically when you run `plain fix` or `plain code check`. If you need to manage them manually:
|
|
140
141
|
|
|
141
142
|
```bash
|
|
142
|
-
# Install
|
|
143
|
+
# Install oxlint/oxfmt (or reinstall if corrupted)
|
|
143
144
|
plain code install
|
|
144
145
|
|
|
145
146
|
# Force reinstall even if up to date
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# plain-code changelog
|
|
2
2
|
|
|
3
|
+
## [0.21.0](https://github.com/dropseed/plain/releases/plain-code@0.21.0) (2026-02-25)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Replaced Biome with oxc tools — JS/TS linting is now handled by [oxlint](https://oxc.rs/) and formatting by [oxfmt](https://oxc.rs/) ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
|
|
8
|
+
- The `--skip-biome` CLI flag is now `--skip-oxc` ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
|
|
9
|
+
- Configuration key changed from `[tool.plain.code.biome]` to `[tool.plain.code.oxc]` in pyproject.toml ([5eb7ba6f6f7f](https://github.com/dropseed/plain/commit/5eb7ba6f6f7f))
|
|
10
|
+
|
|
11
|
+
### Upgrade instructions
|
|
12
|
+
|
|
13
|
+
- If you have `[tool.plain.code.biome]` in your pyproject.toml, rename it to `[tool.plain.code.oxc]`.
|
|
14
|
+
- Replace `--skip-biome` with `--skip-oxc` in any scripts or CI configuration.
|
|
15
|
+
- The oxlint and oxfmt binaries will be downloaded automatically on first run.
|
|
16
|
+
|
|
17
|
+
## [0.20.3](https://github.com/dropseed/plain/releases/plain-code@0.20.3) (2026-02-04)
|
|
18
|
+
|
|
19
|
+
### What's changed
|
|
20
|
+
|
|
21
|
+
- Removed `@internalcode` decorator from `Biome` class ([e7164d3891b2](https://github.com/dropseed/plain/commit/e7164d3891b2))
|
|
22
|
+
|
|
23
|
+
### Upgrade instructions
|
|
24
|
+
|
|
25
|
+
- No changes required.
|
|
26
|
+
|
|
3
27
|
## [0.20.2](https://github.com/dropseed/plain/releases/plain-code@0.20.2) (2026-01-28)
|
|
4
28
|
|
|
5
29
|
### What's changed
|
|
@@ -17,9 +17,10 @@ Plain.code provides comprehensive code quality tools with sensible defaults:
|
|
|
17
17
|
|
|
18
18
|
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
19
19
|
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
20
|
-
- **[
|
|
20
|
+
- **[oxlint](https://oxc.rs/)** - JavaScript and TypeScript linting
|
|
21
|
+
- **[oxfmt](https://oxc.rs/)** - JavaScript, TypeScript, JSON, and CSS formatting
|
|
21
22
|
|
|
22
|
-
Ruff and ty are installed as Python dependencies.
|
|
23
|
+
Ruff and ty are installed as Python dependencies. oxlint and oxfmt are managed automatically as standalone binaries (npm is not required).
|
|
23
24
|
|
|
24
25
|
## Commands
|
|
25
26
|
|
|
@@ -58,10 +59,10 @@ You can skip specific tools if needed:
|
|
|
58
59
|
plain code check --skip-ty
|
|
59
60
|
|
|
60
61
|
# Only run type checks
|
|
61
|
-
plain code check --skip-ruff --skip-
|
|
62
|
+
plain code check --skip-ruff --skip-oxc
|
|
62
63
|
|
|
63
|
-
# Skip
|
|
64
|
-
plain code check --skip-
|
|
64
|
+
# Skip oxlint and oxfmt checks
|
|
65
|
+
plain code check --skip-oxc
|
|
65
66
|
|
|
66
67
|
# Skip annotation coverage checks
|
|
67
68
|
plain code check --skip-annotations
|
|
@@ -93,7 +94,7 @@ plain code annotations --json
|
|
|
93
94
|
|
|
94
95
|
## Settings
|
|
95
96
|
|
|
96
|
-
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml)
|
|
97
|
+
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml).
|
|
97
98
|
|
|
98
99
|
You can customize the behavior in your `pyproject.toml`:
|
|
99
100
|
|
|
@@ -104,9 +105,9 @@ exclude = ["path/to/exclude"]
|
|
|
104
105
|
[tool.plain.code.ty]
|
|
105
106
|
enabled = true # Set to false to disable ty
|
|
106
107
|
|
|
107
|
-
[tool.plain.code.
|
|
108
|
-
enabled = true # Set to false to disable
|
|
109
|
-
version = "1.
|
|
108
|
+
[tool.plain.code.oxc]
|
|
109
|
+
enabled = true # Set to false to disable oxlint/oxfmt
|
|
110
|
+
version = "1.43.0" # Pin to a specific version
|
|
110
111
|
|
|
111
112
|
[tool.plain.code.annotations]
|
|
112
113
|
enabled = true # Set to false to disable annotation checks
|
|
@@ -115,16 +116,16 @@ exclude = ["migrations"] # Exclude specific patterns
|
|
|
115
116
|
|
|
116
117
|
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
117
118
|
|
|
118
|
-
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty,
|
|
119
|
+
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, oxlint, oxfmt) directly instead.
|
|
119
120
|
|
|
120
121
|
## FAQs
|
|
121
122
|
|
|
122
|
-
#### How do I install or update
|
|
123
|
+
#### How do I install or update oxlint/oxfmt manually?
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
oxlint and oxfmt are installed automatically when you run `plain fix` or `plain code check`. If you need to manage them manually:
|
|
125
126
|
|
|
126
127
|
```bash
|
|
127
|
-
# Install
|
|
128
|
+
# Install oxlint/oxfmt (or reinstall if corrupted)
|
|
128
129
|
plain code install
|
|
129
130
|
|
|
130
131
|
# Force reinstall even if up to date
|
|
@@ -11,7 +11,7 @@ paths:
|
|
|
11
11
|
uv run plain fix [path]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Automatically fixes formatting and linting issues using ruff and
|
|
14
|
+
Automatically fixes formatting and linting issues using ruff and oxlint/oxfmt.
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
|
|
@@ -24,7 +24,7 @@ Options:
|
|
|
24
24
|
uv run plain code check [path]
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Runs ruff, ty (type checking),
|
|
27
|
+
Runs ruff, ty (type checking), oxlint/oxfmt, and annotation coverage checks without auto-fixing.
|
|
28
28
|
|
|
29
29
|
## Code Style
|
|
30
30
|
|
|
@@ -14,7 +14,7 @@ from plain.cli.print import print_event
|
|
|
14
14
|
from plain.cli.runtime import common_command, without_runtime_setup
|
|
15
15
|
|
|
16
16
|
from .annotations import AnnotationResult, check_annotations
|
|
17
|
-
from .
|
|
17
|
+
from .oxc import OxcTool, install_oxc
|
|
18
18
|
|
|
19
19
|
DEFAULT_RUFF_CONFIG = Path(__file__).parent / "ruff_defaults.toml"
|
|
20
20
|
|
|
@@ -32,45 +32,44 @@ def cli() -> None:
|
|
|
32
32
|
@click.option("--force", is_flag=True, help="Reinstall even if up to date")
|
|
33
33
|
@click.pass_context
|
|
34
34
|
def install(ctx: click.Context, force: bool) -> None:
|
|
35
|
-
"""Install or update
|
|
35
|
+
"""Install or update oxlint and oxfmt binaries"""
|
|
36
36
|
config = get_code_config()
|
|
37
37
|
|
|
38
|
-
if not config.get("
|
|
39
|
-
click.secho("
|
|
38
|
+
if not config.get("oxc", {}).get("enabled", True):
|
|
39
|
+
click.secho("Oxc is disabled in configuration", fg="yellow")
|
|
40
40
|
return
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
oxlint = OxcTool("oxlint")
|
|
43
43
|
|
|
44
|
-
if force or not
|
|
45
|
-
version_to_install = config.get("
|
|
44
|
+
if force or not oxlint.is_installed() or oxlint.needs_update():
|
|
45
|
+
version_to_install = config.get("oxc", {}).get("version", "")
|
|
46
46
|
if version_to_install:
|
|
47
47
|
click.secho(
|
|
48
|
-
f"Installing
|
|
48
|
+
f"Installing oxlint and oxfmt {version_to_install}...",
|
|
49
49
|
bold=True,
|
|
50
50
|
nl=False,
|
|
51
51
|
)
|
|
52
|
-
installed =
|
|
53
|
-
click.secho(f"
|
|
52
|
+
installed = install_oxc(version_to_install)
|
|
53
|
+
click.secho(f"oxlint and oxfmt {installed} installed", fg="green")
|
|
54
54
|
else:
|
|
55
55
|
ctx.invoke(update)
|
|
56
56
|
else:
|
|
57
|
-
click.secho("
|
|
57
|
+
click.secho("oxlint and oxfmt already installed", fg="green")
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
@without_runtime_setup
|
|
61
61
|
@cli.command()
|
|
62
62
|
def update() -> None:
|
|
63
|
-
"""Update
|
|
63
|
+
"""Update oxlint and oxfmt to latest version"""
|
|
64
64
|
config = get_code_config()
|
|
65
65
|
|
|
66
|
-
if not config.get("
|
|
67
|
-
click.secho("
|
|
66
|
+
if not config.get("oxc", {}).get("enabled", True):
|
|
67
|
+
click.secho("Oxc is disabled in configuration", fg="yellow")
|
|
68
68
|
return
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
version =
|
|
73
|
-
click.secho(f"Biome {version} installed", fg="green")
|
|
70
|
+
click.secho("Updating oxlint and oxfmt...", bold=True)
|
|
71
|
+
version = install_oxc()
|
|
72
|
+
click.secho(f"oxlint and oxfmt {version} installed", fg="green")
|
|
74
73
|
|
|
75
74
|
|
|
76
75
|
@without_runtime_setup
|
|
@@ -79,14 +78,14 @@ def update() -> None:
|
|
|
79
78
|
@click.argument("path", default=".")
|
|
80
79
|
@click.option("--skip-ruff", is_flag=True, help="Skip Ruff checks")
|
|
81
80
|
@click.option("--skip-ty", is_flag=True, help="Skip ty type checks")
|
|
82
|
-
@click.option("--skip-
|
|
81
|
+
@click.option("--skip-oxc", is_flag=True, help="Skip oxlint and oxfmt checks")
|
|
83
82
|
@click.option("--skip-annotations", is_flag=True, help="Skip type annotation checks")
|
|
84
83
|
def check(
|
|
85
84
|
ctx: click.Context,
|
|
86
85
|
path: str,
|
|
87
86
|
skip_ruff: bool,
|
|
88
87
|
skip_ty: bool,
|
|
89
|
-
|
|
88
|
+
skip_oxc: bool,
|
|
90
89
|
skip_annotations: bool,
|
|
91
90
|
) -> None:
|
|
92
91
|
"""Check for formatting and linting issues"""
|
|
@@ -122,14 +121,19 @@ def check(
|
|
|
122
121
|
result = subprocess.run(ty_args)
|
|
123
122
|
maybe_exit(result.returncode)
|
|
124
123
|
|
|
125
|
-
if not
|
|
126
|
-
|
|
124
|
+
if not skip_oxc and config.get("oxc", {}).get("enabled", True):
|
|
125
|
+
oxlint = OxcTool("oxlint")
|
|
126
|
+
oxfmt = OxcTool("oxfmt")
|
|
127
127
|
|
|
128
|
-
if
|
|
128
|
+
if oxlint.needs_update():
|
|
129
129
|
ctx.invoke(install)
|
|
130
130
|
|
|
131
|
-
print_event("
|
|
132
|
-
result =
|
|
131
|
+
print_event("oxlint...", newline=False)
|
|
132
|
+
result = oxlint.invoke(path)
|
|
133
|
+
maybe_exit(result.returncode)
|
|
134
|
+
|
|
135
|
+
print_event("oxfmt --check...", newline=False)
|
|
136
|
+
result = oxfmt.invoke("--check", path)
|
|
133
137
|
maybe_exit(result.returncode)
|
|
134
138
|
|
|
135
139
|
if not skip_annotations and config.get("annotations", {}).get("enabled", True):
|
|
@@ -276,21 +280,25 @@ def fix(ctx: click.Context, path: str, unsafe_fixes: bool, add_noqa: bool) -> No
|
|
|
276
280
|
if result.returncode != 0:
|
|
277
281
|
sys.exit(result.returncode)
|
|
278
282
|
|
|
279
|
-
if config.get("
|
|
280
|
-
|
|
283
|
+
if config.get("oxc", {}).get("enabled", True):
|
|
284
|
+
oxlint = OxcTool("oxlint")
|
|
285
|
+
oxfmt = OxcTool("oxfmt")
|
|
281
286
|
|
|
282
|
-
if
|
|
287
|
+
if oxlint.needs_update():
|
|
283
288
|
ctx.invoke(install)
|
|
284
289
|
|
|
285
|
-
args = ["check", path, "--write"]
|
|
286
|
-
|
|
287
290
|
if unsafe_fixes:
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
print_event("oxlint --fix-dangerously...", newline=False)
|
|
292
|
+
result = oxlint.invoke(path, "--fix-dangerously")
|
|
290
293
|
else:
|
|
291
|
-
print_event("
|
|
294
|
+
print_event("oxlint --fix...", newline=False)
|
|
295
|
+
result = oxlint.invoke(path, "--fix")
|
|
296
|
+
|
|
297
|
+
if result.returncode != 0:
|
|
298
|
+
sys.exit(result.returncode)
|
|
292
299
|
|
|
293
|
-
|
|
300
|
+
print_event("oxfmt...", newline=False)
|
|
301
|
+
result = oxfmt.invoke(path)
|
|
294
302
|
|
|
295
303
|
if result.returncode != 0:
|
|
296
304
|
sys.exit(result.returncode)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Oxc standalone binary management for plain-code.
|
|
3
|
+
|
|
4
|
+
Downloads and manages oxlint (linter) and oxfmt (formatter) binaries
|
|
5
|
+
from the oxc-project/oxc GitHub releases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import subprocess
|
|
14
|
+
import tarfile
|
|
15
|
+
import zipfile
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
import requests
|
|
19
|
+
import tomlkit
|
|
20
|
+
|
|
21
|
+
from plain.runtime import PLAIN_TEMP_PATH
|
|
22
|
+
|
|
23
|
+
TAG_PREFIX = "apps_v"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OxcTool:
|
|
27
|
+
"""Download, install, and invoke an Oxc CLI binary (oxlint or oxfmt)."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str) -> None:
|
|
30
|
+
if name not in ("oxlint", "oxfmt"):
|
|
31
|
+
raise ValueError(f"Unknown Oxc tool: {name}")
|
|
32
|
+
self.name = name
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def target_directory(self) -> str:
|
|
36
|
+
return str(PLAIN_TEMP_PATH)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def standalone_path(self) -> str:
|
|
40
|
+
exe = ".exe" if platform.system() == "Windows" else ""
|
|
41
|
+
return os.path.join(self.target_directory, f"{self.name}{exe}")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def version_lockfile_path(self) -> str:
|
|
45
|
+
return os.path.join(self.target_directory, "oxc.version")
|
|
46
|
+
|
|
47
|
+
def is_installed(self) -> bool:
|
|
48
|
+
td = self.target_directory
|
|
49
|
+
if not os.path.isdir(td):
|
|
50
|
+
os.makedirs(td, exist_ok=True)
|
|
51
|
+
return os.path.exists(self.standalone_path)
|
|
52
|
+
|
|
53
|
+
def needs_update(self) -> bool:
|
|
54
|
+
if not self.is_installed():
|
|
55
|
+
return True
|
|
56
|
+
if not os.path.exists(self.version_lockfile_path):
|
|
57
|
+
return True
|
|
58
|
+
with open(self.version_lockfile_path) as f:
|
|
59
|
+
locked = f.read().strip()
|
|
60
|
+
return locked != self.get_version_from_config()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def get_version_from_config() -> str:
|
|
64
|
+
project_root = os.path.dirname(str(PLAIN_TEMP_PATH))
|
|
65
|
+
pyproject = os.path.join(project_root, "pyproject.toml")
|
|
66
|
+
if not os.path.exists(pyproject):
|
|
67
|
+
return ""
|
|
68
|
+
doc = tomlkit.loads(open(pyproject, "rb").read().decode())
|
|
69
|
+
return (
|
|
70
|
+
doc.get("tool", {})
|
|
71
|
+
.get("plain", {})
|
|
72
|
+
.get("code", {})
|
|
73
|
+
.get("oxc", {})
|
|
74
|
+
.get("version", "")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def set_version_in_config(version: str) -> None:
|
|
79
|
+
project_root = os.path.dirname(str(PLAIN_TEMP_PATH))
|
|
80
|
+
pyproject = os.path.join(project_root, "pyproject.toml")
|
|
81
|
+
if not os.path.exists(pyproject):
|
|
82
|
+
return
|
|
83
|
+
doc = tomlkit.loads(open(pyproject, "rb").read().decode())
|
|
84
|
+
doc.setdefault("tool", {}).setdefault("plain", {}).setdefault(
|
|
85
|
+
"code", {}
|
|
86
|
+
).setdefault("oxc", {})["version"] = version
|
|
87
|
+
open(pyproject, "w").write(tomlkit.dumps(doc))
|
|
88
|
+
|
|
89
|
+
def detect_platform_slug(self) -> str:
|
|
90
|
+
system = platform.system()
|
|
91
|
+
arch = platform.machine()
|
|
92
|
+
if system == "Windows":
|
|
93
|
+
if arch.lower() in ("arm64", "aarch64"):
|
|
94
|
+
return "aarch64-pc-windows-msvc"
|
|
95
|
+
return "x86_64-pc-windows-msvc"
|
|
96
|
+
if system == "Linux":
|
|
97
|
+
if arch == "aarch64":
|
|
98
|
+
return "aarch64-unknown-linux-gnu"
|
|
99
|
+
return "x86_64-unknown-linux-gnu"
|
|
100
|
+
if system == "Darwin":
|
|
101
|
+
if arch == "arm64":
|
|
102
|
+
return "aarch64-apple-darwin"
|
|
103
|
+
return "x86_64-apple-darwin"
|
|
104
|
+
raise RuntimeError(f"Unsupported platform for Oxc: {system}/{arch}")
|
|
105
|
+
|
|
106
|
+
def download(self, version: str = "") -> str:
|
|
107
|
+
slug = self.detect_platform_slug()
|
|
108
|
+
is_windows = platform.system() == "Windows"
|
|
109
|
+
ext = "zip" if is_windows else "tar.gz"
|
|
110
|
+
asset = f"{self.name}-{slug}.{ext}"
|
|
111
|
+
|
|
112
|
+
if version:
|
|
113
|
+
url = f"https://github.com/oxc-project/oxc/releases/download/{TAG_PREFIX}{version}/{asset}"
|
|
114
|
+
else:
|
|
115
|
+
url = f"https://github.com/oxc-project/oxc/releases/latest/download/{asset}"
|
|
116
|
+
|
|
117
|
+
resp = requests.get(url, stream=True)
|
|
118
|
+
resp.raise_for_status()
|
|
119
|
+
|
|
120
|
+
td = self.target_directory
|
|
121
|
+
if not os.path.isdir(td):
|
|
122
|
+
os.makedirs(td, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
# Download into memory for extraction
|
|
125
|
+
data = io.BytesIO()
|
|
126
|
+
total = int(resp.headers.get("Content-Length", 0))
|
|
127
|
+
if total:
|
|
128
|
+
with click.progressbar(
|
|
129
|
+
length=total,
|
|
130
|
+
label=f"Downloading {self.name}",
|
|
131
|
+
width=0,
|
|
132
|
+
) as bar:
|
|
133
|
+
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
|
134
|
+
data.write(chunk)
|
|
135
|
+
bar.update(len(chunk))
|
|
136
|
+
else:
|
|
137
|
+
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
|
138
|
+
data.write(chunk)
|
|
139
|
+
|
|
140
|
+
data.seek(0)
|
|
141
|
+
|
|
142
|
+
# Extract the binary from the archive
|
|
143
|
+
if is_windows:
|
|
144
|
+
with zipfile.ZipFile(data) as zf:
|
|
145
|
+
# Find the binary inside the archive
|
|
146
|
+
members = zf.namelist()
|
|
147
|
+
binary_name = next(m for m in members if m.startswith(self.name))
|
|
148
|
+
with (
|
|
149
|
+
zf.open(binary_name) as src,
|
|
150
|
+
open(self.standalone_path, "wb") as dst,
|
|
151
|
+
):
|
|
152
|
+
dst.write(src.read())
|
|
153
|
+
else:
|
|
154
|
+
with tarfile.open(fileobj=data, mode="r:gz") as tf:
|
|
155
|
+
members = tf.getnames()
|
|
156
|
+
binary_name = next(m for m in members if m.startswith(self.name))
|
|
157
|
+
extracted = tf.extractfile(binary_name)
|
|
158
|
+
if extracted is None:
|
|
159
|
+
raise RuntimeError(f"Failed to extract {binary_name} from archive")
|
|
160
|
+
with open(self.standalone_path, "wb") as dst:
|
|
161
|
+
dst.write(extracted.read())
|
|
162
|
+
|
|
163
|
+
os.chmod(self.standalone_path, 0o755)
|
|
164
|
+
|
|
165
|
+
# Determine resolved version for lockfile
|
|
166
|
+
if version:
|
|
167
|
+
resolved = version.lstrip("v")
|
|
168
|
+
else:
|
|
169
|
+
resolved = ""
|
|
170
|
+
if resp.history:
|
|
171
|
+
loc = resp.history[0].headers.get("Location", "")
|
|
172
|
+
if TAG_PREFIX in loc:
|
|
173
|
+
remaining = loc.split(TAG_PREFIX, 1)[-1]
|
|
174
|
+
resolved = remaining.split("/")[0]
|
|
175
|
+
|
|
176
|
+
if not resolved:
|
|
177
|
+
raise RuntimeError("Failed to determine resolved version from redirect")
|
|
178
|
+
|
|
179
|
+
return resolved
|
|
180
|
+
|
|
181
|
+
def invoke(self, *args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
|
|
182
|
+
config_path = os.path.join(
|
|
183
|
+
os.path.dirname(__file__), f"{self.name}_defaults.json"
|
|
184
|
+
)
|
|
185
|
+
extra_args = ["-c", config_path]
|
|
186
|
+
return subprocess.run([self.standalone_path, *extra_args, *args], cwd=cwd)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def install_oxc(version: str = "") -> str:
|
|
190
|
+
"""Install both oxlint and oxfmt, return the resolved version."""
|
|
191
|
+
oxlint = OxcTool("oxlint")
|
|
192
|
+
oxfmt = OxcTool("oxfmt")
|
|
193
|
+
|
|
194
|
+
resolved = oxlint.download(version)
|
|
195
|
+
oxfmt.download(resolved)
|
|
196
|
+
|
|
197
|
+
# Write version lockfile once (shared by both tools)
|
|
198
|
+
with open(oxlint.version_lockfile_path, "w") as f:
|
|
199
|
+
f.write(resolved)
|
|
200
|
+
|
|
201
|
+
OxcTool.set_version_in_config(resolved)
|
|
202
|
+
return resolved
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
target-version = "py311"
|
|
2
|
+
|
|
3
|
+
[lint]
|
|
4
|
+
ignore = [
|
|
5
|
+
"E501", # Never enforce `E501` (line length violations)
|
|
6
|
+
"S101", # pytest use of assert
|
|
7
|
+
"ISC001", # Implicit string concatenation
|
|
8
|
+
]
|
|
9
|
+
extend-select = [
|
|
10
|
+
"I", # isort
|
|
11
|
+
# # "C90", # mccabe
|
|
12
|
+
# # "N", # pep8-naming
|
|
13
|
+
"UP", # pyupgrade
|
|
14
|
+
# "S", # bandit
|
|
15
|
+
# # "B", # bugbear
|
|
16
|
+
"C4", # flake8-comprehensions
|
|
17
|
+
# # "DTZ", # flake8-datetimez
|
|
18
|
+
"ISC", # flake8-implicit-str-concat
|
|
19
|
+
# # "G", # flake8-logging-format
|
|
20
|
+
# # "T20", # print
|
|
21
|
+
"PT", # pytest
|
|
22
|
+
"B006", # mutable-argument-default
|
|
23
|
+
]
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "plain.code"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.21.0"
|
|
4
4
|
description = "Preconfigured code formatting and linting."
|
|
5
|
-
authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
|
|
5
|
+
authors = [{ name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev" }]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "BSD-3-Clause"
|
|
8
8
|
requires-python = ">=3.13"
|
|
9
|
-
dependencies = [
|
|
10
|
-
"plain<1.0.0",
|
|
11
|
-
"ruff>=0.1.0",
|
|
12
|
-
"ty>=0.0.11",
|
|
13
|
-
"requests>=2.0.0",
|
|
14
|
-
"tomlkit>=0.11.0",
|
|
15
|
-
]
|
|
9
|
+
dependencies = ["plain<1.0.0", "ruff>=0.1.0", "ty>=0.0.11", "requests>=2.0.0", "tomlkit>=0.11.0"]
|
|
16
10
|
|
|
17
11
|
# Make this available as a standalone command
|
|
18
12
|
# in case plain can't load or something (this can run anyways)
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Biome standalone binary management for plain-code.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import platform
|
|
7
|
-
import subprocess
|
|
8
|
-
|
|
9
|
-
import click
|
|
10
|
-
import requests
|
|
11
|
-
import tomlkit
|
|
12
|
-
|
|
13
|
-
from plain.internal import internalcode
|
|
14
|
-
from plain.runtime import PLAIN_TEMP_PATH
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@internalcode
|
|
18
|
-
class Biome:
|
|
19
|
-
"""Download, install, and invoke the Biome CLI standalone binary."""
|
|
20
|
-
|
|
21
|
-
TAG_PREFIX = "@biomejs/biome@"
|
|
22
|
-
|
|
23
|
-
@property
|
|
24
|
-
def target_directory(self) -> str:
|
|
25
|
-
# Directory under .plain to store the binary and lockfile
|
|
26
|
-
return str(PLAIN_TEMP_PATH)
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def standalone_path(self) -> str:
|
|
30
|
-
# On Windows, use .exe suffix
|
|
31
|
-
exe = ".exe" if platform.system() == "Windows" else ""
|
|
32
|
-
return os.path.join(self.target_directory, f"biome{exe}")
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def version_lockfile_path(self) -> str:
|
|
36
|
-
return os.path.join(self.target_directory, "biome.version")
|
|
37
|
-
|
|
38
|
-
def is_installed(self) -> bool:
|
|
39
|
-
td = self.target_directory
|
|
40
|
-
if not os.path.isdir(td):
|
|
41
|
-
os.makedirs(td, exist_ok=True)
|
|
42
|
-
return os.path.exists(self.standalone_path)
|
|
43
|
-
|
|
44
|
-
def needs_update(self) -> bool:
|
|
45
|
-
if not self.is_installed():
|
|
46
|
-
return True
|
|
47
|
-
if not os.path.exists(self.version_lockfile_path):
|
|
48
|
-
return True
|
|
49
|
-
with open(self.version_lockfile_path) as f:
|
|
50
|
-
locked = f.read().strip()
|
|
51
|
-
return locked != self.get_version_from_config()
|
|
52
|
-
|
|
53
|
-
def get_version_from_config(self) -> str:
|
|
54
|
-
# Read version from pyproject.toml under tool.plain.code.biome
|
|
55
|
-
project_root = os.path.dirname(self.target_directory)
|
|
56
|
-
pyproject = os.path.join(project_root, "pyproject.toml")
|
|
57
|
-
if not os.path.exists(pyproject):
|
|
58
|
-
return ""
|
|
59
|
-
doc = tomlkit.loads(open(pyproject, "rb").read().decode())
|
|
60
|
-
return (
|
|
61
|
-
doc.get("tool", {})
|
|
62
|
-
.get("plain", {})
|
|
63
|
-
.get("code", {})
|
|
64
|
-
.get("biome", {})
|
|
65
|
-
.get("version", "")
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
def set_version_in_config(self, version: str) -> None:
|
|
69
|
-
# Persist version to pyproject.toml under tool.plain.code.biome
|
|
70
|
-
project_root = os.path.dirname(self.target_directory)
|
|
71
|
-
pyproject = os.path.join(project_root, "pyproject.toml")
|
|
72
|
-
if not os.path.exists(pyproject):
|
|
73
|
-
return
|
|
74
|
-
doc = tomlkit.loads(open(pyproject, "rb").read().decode())
|
|
75
|
-
doc.setdefault("tool", {}).setdefault("plain", {}).setdefault(
|
|
76
|
-
"code", {}
|
|
77
|
-
).setdefault("biome", {})["version"] = version
|
|
78
|
-
open(pyproject, "w").write(tomlkit.dumps(doc))
|
|
79
|
-
|
|
80
|
-
def detect_platform_slug(self) -> str:
|
|
81
|
-
# Determine the asset slug for the current OS/arch
|
|
82
|
-
system = platform.system()
|
|
83
|
-
arch = platform.machine()
|
|
84
|
-
if system == "Windows":
|
|
85
|
-
# use win32 glibc build
|
|
86
|
-
return "win32-arm64.exe" if arch.lower() == "arm64" else "win32-x64.exe"
|
|
87
|
-
if system == "Linux":
|
|
88
|
-
# prefer glibc builds
|
|
89
|
-
return "linux-arm64" if arch == "aarch64" else "linux-x64"
|
|
90
|
-
if system == "Darwin":
|
|
91
|
-
return "darwin-arm64" if arch == "arm64" else "darwin-x64"
|
|
92
|
-
raise RuntimeError(f"Unsupported platform for Biome: {system}/{arch}")
|
|
93
|
-
|
|
94
|
-
def download(self, version: str = "") -> str:
|
|
95
|
-
# Build download URL based on version (tag: cli/vX.Y.Z) or latest
|
|
96
|
-
slug = self.detect_platform_slug()
|
|
97
|
-
if version:
|
|
98
|
-
url = (
|
|
99
|
-
f"https://github.com/biomejs/biome/releases/download/{self.TAG_PREFIX}{version}/"
|
|
100
|
-
f"biome-{slug}"
|
|
101
|
-
)
|
|
102
|
-
else:
|
|
103
|
-
url = (
|
|
104
|
-
f"https://github.com/biomejs/biome/releases/latest/download/"
|
|
105
|
-
f"biome-{slug}"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
resp = requests.get(url, stream=True)
|
|
109
|
-
resp.raise_for_status()
|
|
110
|
-
|
|
111
|
-
# Make sure the target directory exists
|
|
112
|
-
td = self.target_directory
|
|
113
|
-
if not os.path.isdir(td):
|
|
114
|
-
os.makedirs(td, exist_ok=True)
|
|
115
|
-
|
|
116
|
-
total = int(resp.headers.get("Content-Length", 0))
|
|
117
|
-
with open(self.standalone_path, "wb") as f:
|
|
118
|
-
if total:
|
|
119
|
-
with click.progressbar(
|
|
120
|
-
length=total,
|
|
121
|
-
label="Downloading Biome",
|
|
122
|
-
width=0,
|
|
123
|
-
) as bar:
|
|
124
|
-
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
|
125
|
-
f.write(chunk)
|
|
126
|
-
bar.update(len(chunk))
|
|
127
|
-
else:
|
|
128
|
-
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
|
129
|
-
f.write(chunk)
|
|
130
|
-
os.chmod(self.standalone_path, 0o755)
|
|
131
|
-
|
|
132
|
-
# Determine resolved version for lockfile
|
|
133
|
-
if version:
|
|
134
|
-
resolved = version.lstrip("v")
|
|
135
|
-
else:
|
|
136
|
-
resolved = ""
|
|
137
|
-
if resp.history:
|
|
138
|
-
# Look for redirect to actual tag version
|
|
139
|
-
loc = resp.history[0].headers.get("Location", "")
|
|
140
|
-
if self.TAG_PREFIX in loc:
|
|
141
|
-
remaining = loc.split(self.TAG_PREFIX, 1)[-1]
|
|
142
|
-
resolved = remaining.split("/")[0]
|
|
143
|
-
|
|
144
|
-
if not resolved:
|
|
145
|
-
raise RuntimeError("Failed to determine resolved version from redirect")
|
|
146
|
-
|
|
147
|
-
open(self.version_lockfile_path, "w").write(resolved)
|
|
148
|
-
|
|
149
|
-
return resolved
|
|
150
|
-
|
|
151
|
-
def install(self, version: str = "") -> str:
|
|
152
|
-
v = self.download(version)
|
|
153
|
-
self.set_version_in_config(v)
|
|
154
|
-
return v
|
|
155
|
-
|
|
156
|
-
def invoke(self, *args: str, cwd: str | None = None) -> subprocess.CompletedProcess:
|
|
157
|
-
# Run the standalone biome binary with given args
|
|
158
|
-
config_path = os.path.abspath(
|
|
159
|
-
os.path.join(os.path.dirname(__file__), "biome_defaults.json")
|
|
160
|
-
)
|
|
161
|
-
args = list(args) + ["--config-path", config_path, "--vcs-root", os.getcwd()]
|
|
162
|
-
return subprocess.run([self.standalone_path, *args], cwd=cwd)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"root": true,
|
|
3
|
-
"vcs": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"clientKind": "git",
|
|
6
|
-
"useIgnoreFile": true
|
|
7
|
-
},
|
|
8
|
-
"files": {
|
|
9
|
-
"includes": [
|
|
10
|
-
"**",
|
|
11
|
-
"!**/vendor/**",
|
|
12
|
-
"!**/node_modules/**",
|
|
13
|
-
"!**/*.min.*",
|
|
14
|
-
"!**/tests/**",
|
|
15
|
-
"!**/htmlcov/**",
|
|
16
|
-
"!**/.venv/**",
|
|
17
|
-
"!**/.pytest_cache/**"
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
"formatter": {
|
|
21
|
-
"indentStyle": "space"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
target-version = "py311"
|
|
2
|
-
|
|
3
|
-
[lint]
|
|
4
|
-
ignore = [
|
|
5
|
-
"E501", # Never enforce `E501` (line length violations)
|
|
6
|
-
"S101", # pytest use of assert
|
|
7
|
-
"ISC001", # Implicit string concatenation
|
|
8
|
-
]
|
|
9
|
-
extend-select = [
|
|
10
|
-
"I", # isort
|
|
11
|
-
# # "C90", # mccabe
|
|
12
|
-
# # "N", # pep8-naming
|
|
13
|
-
"UP", # pyupgrade
|
|
14
|
-
# "S", # bandit
|
|
15
|
-
# # "B", # bugbear
|
|
16
|
-
"C4", # flake8-comprehensions
|
|
17
|
-
# # "DTZ", # flake8-datetimez
|
|
18
|
-
"ISC", # flake8-implicit-str-concat
|
|
19
|
-
# # "G", # flake8-logging-format
|
|
20
|
-
# # "T20", # print
|
|
21
|
-
"PT", # pytest
|
|
22
|
-
"B006", # mutable-argument-default
|
|
23
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|