cssupgrade 0.0.0-stage → 2026.0.0
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.
- package/.claude-plugin/plugin.json +22 -0
- package/.editorconfig +27 -0
- package/.gitattributes +1 -0
- package/.github/agents/superjoe.agent.md +65 -0
- package/.github/copilot-instructions.md +32 -0
- package/.github/dependabot.yml +10 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/pr.yml +27 -0
- package/.github/workflows/release.yml +22 -0
- package/.idea/cssupgrade.iml +14 -0
- package/.idea/inspectionProfiles/Project_Default.xml +28 -0
- package/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- package/.idea/misc.xml +4 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.oxfmtrc.json +3 -0
- package/.pre-commit-config.yaml +52 -0
- package/.pre-commit-hooks.yaml +8 -0
- package/AGENTS.md +54 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +24 -0
- package/README.md +173 -2
- package/bin/cssupgrade.js +320 -0
- package/images/logo-dark.svg +10 -0
- package/images/logo-light.svg +10 -0
- package/package.json +34 -4
- package/skills/cssupgrade/SKILL.md +16 -0
- package/src/index.js +53 -0
- package/src/newlyAvailable.js +1 -0
- package/src/parser.js +43 -0
- package/src/pool.js +165 -0
- package/src/prefilters.js +29 -0
- package/src/widelyAvailable/cssNesting.js +918 -0
- package/src/widelyAvailable.js +1 -0
- package/src/worker.js +31 -0
- package/tests/baseline.test.js +51 -0
- package/tests/cli.test.js +229 -0
- package/tests/css-nesting.test.js +838 -0
- package/tests/fixtures/silent-worker.js +5 -0
- package/tests/general.test.js +42 -0
- package/tests/parser.test.js +43 -0
- package/tests/pool.test.js +242 -0
- package/tests/prefilters.test.js +24 -0
package/README.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="./images/logo-dark.svg?v=2">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="./images/logo-light.svg?v=2">
|
|
5
|
+
<img alt="CSSupgrade: Auto-upgrade your CSS syntax" src="./images/logo-light.svg?v=2">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
# CSSupgrade [](https://www.npmjs.com/package/cssupgrade) [](https://codecov.io/gh/codingjoe/cssupgrade) [](https://github.com/codingjoe/cssupgrade/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
Keeping your CSS up to date with full browser compatibility.
|
|
12
|
+
|
|
13
|
+
## Sponsors
|
|
14
|
+
|
|
15
|
+
[](https://github.com/sponsors/codingjoe)
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
CSSupgrade is safe and meant to be used automatically on your codebase.
|
|
20
|
+
We recommend integrating it into your development workflow using [pre-commit], [husky], or as an agent skill.
|
|
21
|
+
|
|
22
|
+
### CLI
|
|
23
|
+
|
|
24
|
+
To try it out on a repository without writing changes, run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx -y cssupgrade $(git ls-files '*.css')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To transform code from standard input, pass `-` as the input path:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
echo '.a { .b { color: red } }' | npx cssupgrade -
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For help with available options:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx cssupgrade --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### pre-commit
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx pre-commit install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# .pre-commit-config.yaml
|
|
50
|
+
repos:
|
|
51
|
+
- repo: https://github.com/codingjoe/cssupgrade
|
|
52
|
+
rev: 2026.1.0 # Use the latest release
|
|
53
|
+
hooks:
|
|
54
|
+
- id: cssupgrade
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pre-commit run cssupgrade --all-files
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Husky
|
|
62
|
+
|
|
63
|
+
Assuming Husky is already initialized and `.husky/pre-commit` already contains `set -e`, append:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.css' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; npx -y cssupgrade --write -- \"\$@\"' sh" >> .husky/pre-commit
|
|
67
|
+
echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.css' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; git add -- \"\$@\"' sh" >> .husky/pre-commit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Agent Skill
|
|
71
|
+
|
|
72
|
+
CSSupgrade is available as a skill in [Claude Code]. The plugin is distributed via the [codingjoe/claude-plugins](https://github.com/codingjoe/claude-plugins) marketplace. To use it:
|
|
73
|
+
|
|
74
|
+
1. Run `/plugin marketplace add codingjoe/claude-plugins`
|
|
75
|
+
2. Run `/plugin install cssupgrade@codingjoe`
|
|
76
|
+
|
|
77
|
+
The skill will analyze your selected styles and suggest transformations based on the Baseline browser support policy.
|
|
78
|
+
|
|
79
|
+
<picture>
|
|
80
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark-dark.svg">
|
|
81
|
+
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark.svg">
|
|
82
|
+
<img alt="Baseline: widely available" src="https://web-platform-dx.github.io/assets/img/baseline-wordmark.svg" height="32" align="right">
|
|
83
|
+
</picture>
|
|
84
|
+
|
|
85
|
+
## Browser Support & Baseline
|
|
86
|
+
|
|
87
|
+
All transformations are based on [Web Platform Baseline][baseline] features. Baseline tracks which web platform features are safe to use across browsers.
|
|
88
|
+
|
|
89
|
+
By default, CSSupgrade uses **widely available** features, meaning they work in all major browsers (Chrome, Edge, Safari, Firefox) for at least 30 months. This ensures full compatibility while keeping your styles modern.
|
|
90
|
+
|
|
91
|
+
You can opt into **newly available** features (available in all browsers for 0-30 months) with:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx cssupgrade --baseline newly-available <files>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For more information about Baseline browser support, visit [web.dev/baseline][baseline].
|
|
98
|
+
|
|
99
|
+
## Supported File Types & Languages
|
|
100
|
+
|
|
101
|
+
- `.css` - CSS stylesheets
|
|
102
|
+
|
|
103
|
+
## Transformations
|
|
104
|
+
|
|
105
|
+
> [!NOTE]
|
|
106
|
+
> A transformation applies only to CSS that the parser accepts. Unknown at-rules
|
|
107
|
+
> and invalid declarations stay unchanged.
|
|
108
|
+
|
|
109
|
+
Rules that cannot nest keep their original text.
|
|
110
|
+
|
|
111
|
+
<picture>
|
|
112
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-widely-word-dark.svg">
|
|
113
|
+
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-widely-word.svg">
|
|
114
|
+
<img alt="Baseline: widely available" src="https://web-platform-dx.github.io/assets/img/baseline-widely-word.svg" height="32" align="right">
|
|
115
|
+
</picture>
|
|
116
|
+
|
|
117
|
+
### Widely available
|
|
118
|
+
|
|
119
|
+
#### Flat rules → [CSS nesting][mdn-nesting]
|
|
120
|
+
|
|
121
|
+
```diff
|
|
122
|
+
.card {
|
|
123
|
+
padding: 1rem;
|
|
124
|
+
-}
|
|
125
|
+
-.card .title { font-weight: bold; }
|
|
126
|
+
-.card:hover { color: red; }
|
|
127
|
+
+ .title { font-weight: bold; }
|
|
128
|
+
+ &:hover { color: red; }
|
|
129
|
+
+}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Only adjacent rules are nested, so the cascade order stays the same.
|
|
133
|
+
Every child selector must repeat a parent selector, and all parent selectors must have the same specificity.
|
|
134
|
+
Rules inside `@media`, `@supports`, `@container`, `@layer`, `@scope`, and `@starting-style` blocks are nested as well.
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
/* Not transformed - `@keyframes` children are keyframe selectors */
|
|
138
|
+
@keyframes fade {
|
|
139
|
+
from { opacity: 0 }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
<picture>
|
|
144
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word-dark.svg">
|
|
145
|
+
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg">
|
|
146
|
+
<img alt="Baseline: Newly available" src="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg" height="32" align="right">
|
|
147
|
+
</picture>
|
|
148
|
+
|
|
149
|
+
### Newly available
|
|
150
|
+
|
|
151
|
+
No transformations are available yet.
|
|
152
|
+
|
|
153
|
+
## Versioning
|
|
154
|
+
|
|
155
|
+
CSSupgrade uses the [calver] `YYYY.MINOR.PATCH` versioning scheme.
|
|
156
|
+
|
|
157
|
+
The year indicates the baseline version. New transformations are added in minor releases, while patches are reserved for bug fixes.
|
|
158
|
+
|
|
159
|
+
## Related Projects
|
|
160
|
+
|
|
161
|
+
Thanks to [esupgrade] for inspiring CSSupgrade.
|
|
162
|
+
|
|
163
|
+
### Distinction
|
|
164
|
+
|
|
165
|
+
[esupgrade] upgrades JavaScript and TypeScript syntax.
|
|
166
|
+
CSSupgrade applies the same Baseline policy to CSS.
|
|
167
|
+
|
|
168
|
+
[baseline]: https://web.dev/baseline/
|
|
169
|
+
[calver]: https://calver.org/
|
|
170
|
+
[claude code]: https://code.claude.com
|
|
171
|
+
[esupgrade]: https://github.com/codingjoe/esupgrade
|
|
172
|
+
[husky]: https://typicode.github.io/husky/
|
|
173
|
+
[mdn-nesting]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting
|
|
174
|
+
[pre-commit]: https://pre-commit.com/
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command, Option } from "commander"
|
|
4
|
+
import { diffLines } from "diff"
|
|
5
|
+
import { once } from "events"
|
|
6
|
+
import fs from "fs/promises"
|
|
7
|
+
import process from "node:process"
|
|
8
|
+
import pkg from "../package.json" with { type: "json" }
|
|
9
|
+
import { transform } from "../src/index.js"
|
|
10
|
+
import { WorkerPool } from "../src/pool.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CLI tool for cssupgrade.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Process a file using a worker thread.
|
|
18
|
+
* @param {string} filePath - Path to the file to process.
|
|
19
|
+
* @param {Object} options - Processing options.
|
|
20
|
+
* @param {string} options.baseline - Baseline level for transformations.
|
|
21
|
+
* @param {boolean} options.check - Whether to only check for changes.
|
|
22
|
+
* @param {boolean} options.write - Whether to write changes to file.
|
|
23
|
+
* @param {boolean} options.verbose - The verbosity level for logging.
|
|
24
|
+
* @param {import("../src/pool.js").TransformWorker} worker - Worker that transforms the file.
|
|
25
|
+
* @returns {Promise<{modified: boolean, error: boolean}>} Result of processing.
|
|
26
|
+
*/
|
|
27
|
+
async function processFile(filePath, options, worker) {
|
|
28
|
+
// Validate that the provided path exists and is a file.
|
|
29
|
+
try {
|
|
30
|
+
const stats = await fs.stat(filePath)
|
|
31
|
+
if (!stats.isFile()) {
|
|
32
|
+
console.error(`Error: '${filePath}' is not a file`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error: Cannot access '${filePath}': ${error.message}`)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// The worker only ships the original source when a diff can be printed.
|
|
42
|
+
const showsDiff = options.check || !options.write
|
|
43
|
+
const workerResult = await worker.transform({
|
|
44
|
+
filePath,
|
|
45
|
+
baseline: options.baseline,
|
|
46
|
+
includeOriginal: showsDiff,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (!workerResult.success) {
|
|
50
|
+
if (options.verbose) console.error(workerResult.error)
|
|
51
|
+
console.error(
|
|
52
|
+
`\x1b[31m✗\x1b[0m Error: ${filePath}: ${workerResult.error.message}`,
|
|
53
|
+
)
|
|
54
|
+
return { modified: false, error: true }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = workerResult.result
|
|
58
|
+
|
|
59
|
+
if (result.modified) {
|
|
60
|
+
if (showsDiff) {
|
|
61
|
+
displayDiff(filePath, result.original, result.code)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (options.write) {
|
|
65
|
+
await fs.writeFile(filePath, result.code, "utf8")
|
|
66
|
+
if (!options.check) {
|
|
67
|
+
console.info(`\x1b[32m✓\x1b[0m ${filePath}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { modified: true, error: false }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Show unmodified files unless in check-only mode
|
|
75
|
+
if (!options.check) {
|
|
76
|
+
console.debug(` ${filePath}`)
|
|
77
|
+
}
|
|
78
|
+
return { modified: false, error: false }
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`\x1b[31m✗\x1b[0m Error: ${filePath}: ${error.message}`)
|
|
81
|
+
return { modified: false, error: true }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Display a diff between original and modified code.
|
|
87
|
+
* @param {string} filePath - Path to the file being displayed.
|
|
88
|
+
* @param {string} original - Original code.
|
|
89
|
+
* @param {string} modified - Modified code.
|
|
90
|
+
*/
|
|
91
|
+
function displayDiff(filePath, original, modified) {
|
|
92
|
+
const changes = diffLines(original, modified).filter(
|
|
93
|
+
({ added, removed }) => added || removed,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
console.group(`\x1b[31m✗\x1b[0m ${filePath}`)
|
|
97
|
+
for (const { added, value } of changes) {
|
|
98
|
+
const prefix = added ? ` \x1b[32m+ ` : ` \x1b[31m- `
|
|
99
|
+
for (const line of value.split("\n")) {
|
|
100
|
+
if (line.trim() !== "") {
|
|
101
|
+
console.info(`${prefix}${line}\x1b[0m`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.groupEnd()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Process stdin using the configured transformation options.
|
|
110
|
+
* @param {Object} options - Processing options.
|
|
111
|
+
* @param {string} options.baseline - Baseline level for transformations.
|
|
112
|
+
* @param {boolean} options.check - Whether to only check for changes.
|
|
113
|
+
* @param {boolean} options.write - Whether to write changes to file.
|
|
114
|
+
* @param {boolean} options.verbose - The verbosity level for logging.
|
|
115
|
+
* @returns {Promise<void>} Complete when stdin processing finishes.
|
|
116
|
+
*/
|
|
117
|
+
async function processStdin(options) {
|
|
118
|
+
if (options.write) {
|
|
119
|
+
console.error("Error: '--write' cannot be used with stdin")
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const code = await readStdin()
|
|
125
|
+
const result = transform(code, options.baseline)
|
|
126
|
+
|
|
127
|
+
if (options.check) {
|
|
128
|
+
if (result.modified) {
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.stdout.write(result.code)
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (options.verbose) console.error(error)
|
|
137
|
+
console.error(`\x1b[31m✗\x1b[0m Error: stdin: ${error.message}`)
|
|
138
|
+
process.exit(128)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Read source code from stdin.
|
|
144
|
+
* @returns {Promise<string>} Source code from stdin.
|
|
145
|
+
*/
|
|
146
|
+
async function readStdin() {
|
|
147
|
+
process.stdin.setEncoding("utf8")
|
|
148
|
+
let code = ""
|
|
149
|
+
|
|
150
|
+
process.stdin.on("data", (chunk) => {
|
|
151
|
+
code += chunk
|
|
152
|
+
})
|
|
153
|
+
await once(process.stdin, "end")
|
|
154
|
+
return code
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Identify a result that changed the file.
|
|
159
|
+
* @param {{modified: boolean}} result - Result of processing one file.
|
|
160
|
+
* @returns {boolean} True when the file changed.
|
|
161
|
+
*/
|
|
162
|
+
function isModified({ modified }) {
|
|
163
|
+
return modified
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Identify a result that failed.
|
|
168
|
+
* @param {{error: boolean}} result - Result of processing one file.
|
|
169
|
+
* @returns {boolean} True when the file failed to process.
|
|
170
|
+
*/
|
|
171
|
+
function isFailed({ error }) {
|
|
172
|
+
return error
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Describe a number of files.
|
|
177
|
+
* @param {number} count - Number of files.
|
|
178
|
+
* @returns {string} Count followed by a singular or plural noun.
|
|
179
|
+
*/
|
|
180
|
+
function formatFileCount(count) {
|
|
181
|
+
return `${count} file${count === 1 ? "" : "s"}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Orchestrates the CLI application.
|
|
186
|
+
*/
|
|
187
|
+
class CLIRunner {
|
|
188
|
+
constructor(workerPath) {
|
|
189
|
+
this.workerPool = new WorkerPool(processFile, workerPath)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Process files and report results.
|
|
194
|
+
* @param {string[]} patterns - File paths to process (no globbing).
|
|
195
|
+
* @param {Object} options - Processing options.
|
|
196
|
+
*/
|
|
197
|
+
async run(patterns, options) {
|
|
198
|
+
switch (this.#getInputMode(patterns)) {
|
|
199
|
+
case "stdin":
|
|
200
|
+
await processStdin(options)
|
|
201
|
+
return
|
|
202
|
+
case "mixed":
|
|
203
|
+
console.error("Error: '-' cannot be combined with file paths")
|
|
204
|
+
return process.exit(1)
|
|
205
|
+
default:
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.time("Processing")
|
|
210
|
+
// Hand CLI-provided names directly to the worker pool; file validation occurs in processFile.
|
|
211
|
+
const results = await this.workerPool.processFiles(patterns, options)
|
|
212
|
+
console.timeEnd("Processing")
|
|
213
|
+
|
|
214
|
+
this.#reportSummary(results, options)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Classify the input mode selected by CLI arguments.
|
|
219
|
+
* @param {string[]} patterns - File paths to process.
|
|
220
|
+
* @returns {"files" | "stdin" | "mixed"} Selected input mode.
|
|
221
|
+
*/
|
|
222
|
+
#getInputMode(patterns) {
|
|
223
|
+
const stdinCount = patterns.filter((pattern) => pattern === "-").length
|
|
224
|
+
|
|
225
|
+
switch (stdinCount) {
|
|
226
|
+
case 0:
|
|
227
|
+
return "files"
|
|
228
|
+
case 1:
|
|
229
|
+
return patterns.length === 1 ? "stdin" : "mixed"
|
|
230
|
+
default:
|
|
231
|
+
return "mixed"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Report the outcome of a file-processing run.
|
|
237
|
+
*
|
|
238
|
+
* @param {Array<{modified: boolean, error: boolean}>} results - Result per file.
|
|
239
|
+
* @param {Object} options - Processing options.
|
|
240
|
+
*/
|
|
241
|
+
#reportSummary(results, options) {
|
|
242
|
+
const modifiedCount = results.filter(isModified).length
|
|
243
|
+
const errorCount = results.filter(isFailed).length
|
|
244
|
+
|
|
245
|
+
console.info("")
|
|
246
|
+
|
|
247
|
+
switch (modifiedCount) {
|
|
248
|
+
case 0:
|
|
249
|
+
if (errorCount === 0) {
|
|
250
|
+
console.info("All files are up to date")
|
|
251
|
+
}
|
|
252
|
+
break
|
|
253
|
+
default:
|
|
254
|
+
this.#reportUpgrades(modifiedCount, options)
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (errorCount > 0) {
|
|
259
|
+
console.info(`${formatFileCount(errorCount)} failed to process`)
|
|
260
|
+
// Errors take precedence over the --check flag.
|
|
261
|
+
process.exit(128)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (options.check && modifiedCount > 0) {
|
|
265
|
+
process.exit(1)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Report how many files were upgraded.
|
|
271
|
+
*
|
|
272
|
+
* @param {number} modifiedCount - Number of files with changes.
|
|
273
|
+
* @param {Object} options - Processing options.
|
|
274
|
+
*/
|
|
275
|
+
#reportUpgrades(modifiedCount, options) {
|
|
276
|
+
switch (true) {
|
|
277
|
+
case options.check:
|
|
278
|
+
console.info(
|
|
279
|
+
`${formatFileCount(modifiedCount)} need${modifiedCount === 1 ? "s" : ""} upgrading`,
|
|
280
|
+
)
|
|
281
|
+
if (options.write) {
|
|
282
|
+
console.info("Changes have been written")
|
|
283
|
+
}
|
|
284
|
+
break
|
|
285
|
+
case options.write:
|
|
286
|
+
console.info(`✓ ${formatFileCount(modifiedCount)} upgraded`)
|
|
287
|
+
break
|
|
288
|
+
default:
|
|
289
|
+
console.info(`${formatFileCount(modifiedCount)} would be upgraded`)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Initialize CLI
|
|
296
|
+
const program = new Command()
|
|
297
|
+
const cliRunner = new CLIRunner(new URL("../src/worker.js", import.meta.url))
|
|
298
|
+
|
|
299
|
+
program
|
|
300
|
+
.name("cssupgrade")
|
|
301
|
+
.description("Auto-upgrade your CSS syntax")
|
|
302
|
+
.version(pkg.version)
|
|
303
|
+
.argument("<files...>", "Files or directories to process, or '-' to read from stdin")
|
|
304
|
+
.addOption(
|
|
305
|
+
new Option("--baseline <level>", "Set baseline level for transformations")
|
|
306
|
+
.choices(["widely-available", "newly-available"])
|
|
307
|
+
.default("widely-available"),
|
|
308
|
+
)
|
|
309
|
+
.option("--verbose, -v", "Show more detailed output.", false)
|
|
310
|
+
.option(
|
|
311
|
+
"--check",
|
|
312
|
+
"Report which files need upgrading and exit with code 1 if any do",
|
|
313
|
+
false,
|
|
314
|
+
)
|
|
315
|
+
.option("--write", "Write changes to files", false)
|
|
316
|
+
.action(async (files, options) => {
|
|
317
|
+
await cliRunner.run(files, options)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
program.parse()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="320" font-family="Segoe UI, system-ui, sans-serif" viewBox="0 0 1280 640">
|
|
2
|
+
<circle cx="260" cy="320" r="200" fill="#2965f1"/>
|
|
3
|
+
<path fill="none" stroke="#ffffff" stroke-width="48" d="m167 409 95-95 95 95m-190 -95 95-95 95 95m415 50"/>
|
|
4
|
+
<text x="241" y="148" fill="#c9d1d9" font-size="75" font-weight="bold" transform="matrix(1.6 0 0 1.6 120 120)">
|
|
5
|
+
CSSupgrade
|
|
6
|
+
</text>
|
|
7
|
+
<text x="241" y="192" fill="#c9d1d9" font-size="32" font-style="italic" transform="matrix(1.6 0 0 1.6 120 120)">
|
|
8
|
+
Auto-upgrade your CSS syntax
|
|
9
|
+
</text>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="320" font-family="Segoe UI, system-ui, sans-serif" viewBox="0 0 1280 640">
|
|
2
|
+
<circle cx="260" cy="320" r="200" fill="#2965f1"/>
|
|
3
|
+
<path fill="none" stroke="#ffffff" stroke-width="48" d="m167 409 95-95 95 95m-190 -95 95-95 95 95m415 50"/>
|
|
4
|
+
<text x="241" y="148" font-size="75" font-weight="bold" transform="matrix(1.6 0 0 1.6 120 120)">
|
|
5
|
+
CSSupgrade
|
|
6
|
+
</text>
|
|
7
|
+
<text x="241" y="192" font-size="32" font-style="italic" transform="matrix(1.6 0 0 1.6 120 120)">
|
|
8
|
+
Auto-upgrade your CSS syntax
|
|
9
|
+
</text>
|
|
10
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cssupgrade",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2026.0.0",
|
|
4
|
+
"description": "Auto-upgrade your CSS syntax",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cssupgrade": "./bin/cssupgrade.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.info --test-coverage-exclude=tests/**/* tests/*.test.js tests/**/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"css",
|
|
15
|
+
"modernize",
|
|
16
|
+
"upgrade",
|
|
17
|
+
"codemod",
|
|
18
|
+
"ast",
|
|
19
|
+
"transform",
|
|
20
|
+
"nesting"
|
|
21
|
+
],
|
|
22
|
+
"author": "Johannes Maron",
|
|
23
|
+
"license": "BSD-2-Clause",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": ">=14.0.2",
|
|
26
|
+
"css-tree": "^3.2.1",
|
|
27
|
+
"diff": ">=8.0.2"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.0.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/codingjoe/cssupgrade.git"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cssupgrade
|
|
3
|
+
description: CSSupgrade auto-updates CSS syntax to new features based on browser support. Use when the user asks to modernize, upgrade, or clean up CSS syntax.
|
|
4
|
+
allowed-tools: Bash(npx -y cssupgrade *) Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use the CLI with `npx` on files or standard input:
|
|
8
|
+
|
|
9
|
+
```console
|
|
10
|
+
npx -y cssupgrade [--baseline <newly-available|widely-available>] [--check] [--write] <files|->
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Use `--check` to preview changes and fail when updates are needed.
|
|
14
|
+
- Use `-` to read from standard input and write the transformed code to standard output.
|
|
15
|
+
- Use `--write` to apply updates in place for file inputs.
|
|
16
|
+
- Use `--baseline` to choose `newly-available` or `widely-available` (default).
|
package/src/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as newlyAvailable from "./newlyAvailable.js"
|
|
2
|
+
import { matchesPrefilter } from "./prefilters.js"
|
|
3
|
+
import * as widelyAvailable from "./widelyAvailable.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} TransformResult
|
|
7
|
+
* @property {string} code - Transformed CSS source.
|
|
8
|
+
* @property {boolean} modified - Whether the source changed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {function(string): TransformResult} Transformer
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {[string, Transformer]} NamedTransformer
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Apply every transformer that passes its pre-filter, then repeat on the result.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} code - CSS source text.
|
|
23
|
+
* @param {NamedTransformer[]} transformers - Transformers to apply.
|
|
24
|
+
* @param {boolean} globalModified - Whether an earlier pass changed the source.
|
|
25
|
+
* @returns {TransformResult} Transformed source.
|
|
26
|
+
*/
|
|
27
|
+
function applyTransformersRecursively(code, transformers, globalModified = false) {
|
|
28
|
+
const passResult = transformers.reduce(
|
|
29
|
+
(result, [name, transformer]) =>
|
|
30
|
+
matchesPrefilter(name, result.code) ? transformer(result.code) : result,
|
|
31
|
+
{ code, modified: false },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return passResult.modified
|
|
35
|
+
? applyTransformersRecursively(passResult.code, transformers, true)
|
|
36
|
+
: { code: passResult.code, modified: globalModified }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Transform CSS using the transformers of the selected baseline.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} code - CSS source text.
|
|
43
|
+
* @param {string} [baseline] - Baseline level ('widely-available' or 'newly-available').
|
|
44
|
+
* @returns {TransformResult} Transformed source.
|
|
45
|
+
*/
|
|
46
|
+
export function transform(code, baseline = "widely-available") {
|
|
47
|
+
const transformers =
|
|
48
|
+
baseline === "newly-available"
|
|
49
|
+
? { ...widelyAvailable, ...newlyAvailable }
|
|
50
|
+
: widelyAvailable
|
|
51
|
+
|
|
52
|
+
return applyTransformersRecursively(code, Object.entries(transformers))
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as csstree from "css-tree"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} SourceRange
|
|
5
|
+
* @property {number} start - Offset of the first character.
|
|
6
|
+
* @property {number} end - Offset after the last character.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} ParsedCss
|
|
11
|
+
* @property {import("css-tree").CssNode} ast - Parsed CSS syntax tree.
|
|
12
|
+
* @property {SourceRange[]} comments - Comment ranges in source order.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Record the range of every comment in the source text.
|
|
17
|
+
*
|
|
18
|
+
* @param {SourceRange[]} comments - Collected comment ranges.
|
|
19
|
+
* @param {string} _value - Comment text without delimiters.
|
|
20
|
+
* @param {{start: {offset: number}, end: {offset: number}}} loc - Comment location.
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
function recordComment(comments, _value, loc) {
|
|
24
|
+
comments.push({ start: loc.start.offset, end: loc.end.offset })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse CSS and collect its comments, which the syntax tree does not include.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} css - CSS source text.
|
|
31
|
+
* @returns {ParsedCss} Syntax tree and comment ranges.
|
|
32
|
+
*/
|
|
33
|
+
export function parseCss(css) {
|
|
34
|
+
const comments = []
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ast: csstree.parse(css, {
|
|
38
|
+
positions: true,
|
|
39
|
+
onComment: recordComment.bind(null, comments),
|
|
40
|
+
}),
|
|
41
|
+
comments,
|
|
42
|
+
}
|
|
43
|
+
}
|