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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/.editorconfig +27 -0
  3. package/.gitattributes +1 -0
  4. package/.github/agents/superjoe.agent.md +65 -0
  5. package/.github/copilot-instructions.md +32 -0
  6. package/.github/dependabot.yml +10 -0
  7. package/.github/workflows/ci.yml +35 -0
  8. package/.github/workflows/pr.yml +27 -0
  9. package/.github/workflows/release.yml +22 -0
  10. package/.idea/cssupgrade.iml +14 -0
  11. package/.idea/inspectionProfiles/Project_Default.xml +28 -0
  12. package/.idea/inspectionProfiles/profiles_settings.xml +6 -0
  13. package/.idea/misc.xml +4 -0
  14. package/.idea/modules.xml +8 -0
  15. package/.idea/vcs.xml +6 -0
  16. package/.oxfmtrc.json +3 -0
  17. package/.pre-commit-config.yaml +52 -0
  18. package/.pre-commit-hooks.yaml +8 -0
  19. package/AGENTS.md +54 -0
  20. package/CONTRIBUTING.md +40 -0
  21. package/LICENSE +24 -0
  22. package/README.md +173 -2
  23. package/bin/cssupgrade.js +320 -0
  24. package/images/logo-dark.svg +10 -0
  25. package/images/logo-light.svg +10 -0
  26. package/package.json +34 -4
  27. package/skills/cssupgrade/SKILL.md +16 -0
  28. package/src/index.js +53 -0
  29. package/src/newlyAvailable.js +1 -0
  30. package/src/parser.js +43 -0
  31. package/src/pool.js +165 -0
  32. package/src/prefilters.js +29 -0
  33. package/src/widelyAvailable/cssNesting.js +918 -0
  34. package/src/widelyAvailable.js +1 -0
  35. package/src/worker.js +31 -0
  36. package/tests/baseline.test.js +51 -0
  37. package/tests/cli.test.js +229 -0
  38. package/tests/css-nesting.test.js +838 -0
  39. package/tests/fixtures/silent-worker.js +5 -0
  40. package/tests/general.test.js +42 -0
  41. package/tests/parser.test.js +43 -0
  42. package/tests/pool.test.js +242 -0
  43. package/tests/prefilters.test.js +24 -0
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
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
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
9
+ # CSSupgrade [![npm version](https://img.shields.io/npm/v/cssupgrade.svg?style=flat-square)](https://www.npmjs.com/package/cssupgrade) [![coverage status](https://img.shields.io/codecov/c/github/codingjoe/cssupgrade/main.svg?style=flat-square)](https://codecov.io/gh/codingjoe/cssupgrade) [![license](https://img.shields.io/npm/l/cssupgrade.svg?style=flat-square)](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
+ [![Sponsors](https://django.the-box.sh/sponsors/codingjoe/cssupgrade.svg)](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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }