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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cssNesting } from "./widelyAvailable/cssNesting.js"
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import { parentPort } from "worker_threads"
|
|
3
|
+
import { transform } from "./index.js"
|
|
4
|
+
|
|
5
|
+
/** Worker thread that transforms files on request. */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert an error into a plain object.
|
|
9
|
+
*
|
|
10
|
+
* @param {Error} error - Error thrown while transforming a file.
|
|
11
|
+
* @returns {{name: string, message: string, stack: string | undefined}} Plain error data.
|
|
12
|
+
*/
|
|
13
|
+
function serializeError({ name, message, stack }) {
|
|
14
|
+
return { name, message, stack }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parentPort.on("message", async ({ filePath, baseline, includeOriginal }) => {
|
|
18
|
+
try {
|
|
19
|
+
const code = await fs.readFile(filePath, "utf8")
|
|
20
|
+
const result = transform(code, baseline)
|
|
21
|
+
|
|
22
|
+
parentPort.postMessage({
|
|
23
|
+
success: true,
|
|
24
|
+
result: result.modified
|
|
25
|
+
? { ...result, original: includeOriginal ? code : undefined }
|
|
26
|
+
: { modified: false },
|
|
27
|
+
})
|
|
28
|
+
} catch (error) {
|
|
29
|
+
parentPort.postMessage({ success: false, error: serializeError(error) })
|
|
30
|
+
}
|
|
31
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { suite, test } from "node:test"
|
|
3
|
+
import * as newlyAvailable from "../src/newlyAvailable.js"
|
|
4
|
+
import * as widelyAvailable from "../src/widelyAvailable.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate the date 30 months after the given date.
|
|
8
|
+
*
|
|
9
|
+
* @param {Date} date - The starting date.
|
|
10
|
+
* @returns {Date} The date 30 months later.
|
|
11
|
+
*/
|
|
12
|
+
function widelyAvailableAt(date) {
|
|
13
|
+
const result = new Date(date)
|
|
14
|
+
result.setUTCMonth(result.getUTCMonth() + 30)
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const currentReleaseDate = new Date(Date.UTC(2026, 9, 1))
|
|
19
|
+
|
|
20
|
+
suite("baseline", () => {
|
|
21
|
+
// Dates can be found on https://github.com/web-platform-dx/web-features named `baseline_low_date`
|
|
22
|
+
test("all transformers have a baselineDate", () => {
|
|
23
|
+
for (const [name, transformer] of Object.entries({
|
|
24
|
+
...widelyAvailable,
|
|
25
|
+
...newlyAvailable,
|
|
26
|
+
})) {
|
|
27
|
+
assert(
|
|
28
|
+
transformer.baselineDate instanceof Date,
|
|
29
|
+
`${name} must have a baselineDate`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("widelyAvailable transformers have been released for at least 30 months", () => {
|
|
35
|
+
for (const [name, transformer] of Object.entries(widelyAvailable)) {
|
|
36
|
+
assert(
|
|
37
|
+
widelyAvailableAt(transformer.baselineDate) <= currentReleaseDate,
|
|
38
|
+
`${name} (${transformer.baselineDate.toISOString().slice(0, 10)}) should be released for at least 30 months`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("newlyAvailable transformers are not yet widely available", () => {
|
|
44
|
+
for (const [name, transformer] of Object.entries(newlyAvailable)) {
|
|
45
|
+
assert(
|
|
46
|
+
widelyAvailableAt(transformer.baselineDate) > currentReleaseDate,
|
|
47
|
+
`${name} (${transformer.baselineDate.toISOString().slice(0, 10)}) should be moved to widelyAvailable`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import fs from "node:fs/promises"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import { after, before, describe, test } from "node:test"
|
|
8
|
+
|
|
9
|
+
const cliPath = fileURLToPath(new URL("../bin/cssupgrade.js", import.meta.url))
|
|
10
|
+
const deepAtRuleCss = `${"@media all {".repeat(10_000)}.a { } .a .b { color: red }${"}".repeat(10_000)}`
|
|
11
|
+
let directory
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run the CLI in a child process.
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} arguments_ - CLI arguments.
|
|
17
|
+
* @param {string} [input] - Standard input.
|
|
18
|
+
* @returns {import("node:child_process").SpawnSyncReturns<string>} Process result.
|
|
19
|
+
*/
|
|
20
|
+
function runCli(arguments_, input) {
|
|
21
|
+
return spawnSync(process.execPath, [cliPath, ...arguments_], {
|
|
22
|
+
input,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write a file in the temporary directory.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} name - File name.
|
|
31
|
+
* @param {string} content - File content.
|
|
32
|
+
* @returns {Promise<string>} Absolute file path.
|
|
33
|
+
*/
|
|
34
|
+
async function writeFile(name, content) {
|
|
35
|
+
const filePath = path.join(directory, name)
|
|
36
|
+
|
|
37
|
+
await fs.writeFile(filePath, content, "utf8")
|
|
38
|
+
return filePath
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("cssupgrade CLI", () => {
|
|
42
|
+
before(async () => {
|
|
43
|
+
directory = await fs.mkdtemp(path.join(tmpdir(), "cssupgrade-"))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
after(async () => {
|
|
47
|
+
await fs.rm(directory, { recursive: true, force: true })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("report files that are up to date", async () => {
|
|
51
|
+
const result = runCli([await writeFile("current.css", ".a { color: red }")])
|
|
52
|
+
|
|
53
|
+
assert.equal(result.status, 0)
|
|
54
|
+
assert.match(result.stdout, /All files are up to date/)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("print a diff without writing by default", async () => {
|
|
58
|
+
const filePath = await writeFile(
|
|
59
|
+
"nested.css",
|
|
60
|
+
".a { color: red } .a .b { color: blue }",
|
|
61
|
+
)
|
|
62
|
+
const result = runCli([filePath])
|
|
63
|
+
|
|
64
|
+
assert.equal(result.status, 0)
|
|
65
|
+
assert.match(result.stdout, /1 file would be upgraded/)
|
|
66
|
+
assert.equal(
|
|
67
|
+
await fs.readFile(filePath, "utf8"),
|
|
68
|
+
".a { color: red } .a .b { color: blue }",
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("fail in check mode when a file needs upgrading", async () => {
|
|
73
|
+
const result = runCli([
|
|
74
|
+
"--check",
|
|
75
|
+
await writeFile("check.css", ".a { color: red } .a .b { color: blue }"),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
assert.equal(result.status, 1)
|
|
79
|
+
assert.match(result.stdout, /1 file needs upgrading/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("write changes", async () => {
|
|
83
|
+
const filePath = await writeFile(
|
|
84
|
+
"write.css",
|
|
85
|
+
".a { color: red } .a .b { color: blue }",
|
|
86
|
+
)
|
|
87
|
+
const result = runCli(["--write", filePath])
|
|
88
|
+
|
|
89
|
+
assert.equal(result.status, 0)
|
|
90
|
+
assert.match(result.stdout, /1 file upgraded/)
|
|
91
|
+
assert.equal(
|
|
92
|
+
await fs.readFile(filePath, "utf8"),
|
|
93
|
+
".a {\n color: red;\n .b {\n color: blue;\n }\n}",
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("write changes in check mode and still fail", async () => {
|
|
98
|
+
const result = runCli([
|
|
99
|
+
"--check",
|
|
100
|
+
"--write",
|
|
101
|
+
await writeFile("check-write.css", ".a { color: red } .a .b { color: blue }"),
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
assert.equal(result.status, 1)
|
|
105
|
+
assert.match(result.stdout, /Changes have been written/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("transform stdin", () => {
|
|
109
|
+
const result = runCli(["-"], ".a { color: red } .a .b { color: blue }")
|
|
110
|
+
|
|
111
|
+
assert.equal(result.status, 0)
|
|
112
|
+
assert.equal(result.stdout, ".a {\n color: red;\n .b {\n color: blue;\n }\n}")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("fail in check mode for stdin changes", () => {
|
|
116
|
+
const result = runCli(["--check", "-"], ".a { color: red } .a .b { color: blue }")
|
|
117
|
+
|
|
118
|
+
assert.equal(result.status, 1)
|
|
119
|
+
assert.equal(result.stdout, "")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("reject --write with stdin", () => {
|
|
123
|
+
const result = runCli(["--write", "-"], ".a { color: red }")
|
|
124
|
+
|
|
125
|
+
assert.equal(result.status, 1)
|
|
126
|
+
assert.match(result.stderr, /cannot be used with stdin/)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("reject stdin combined with file paths", () => {
|
|
130
|
+
const result = runCli(["-", "file.css"], "")
|
|
131
|
+
|
|
132
|
+
assert.equal(result.status, 1)
|
|
133
|
+
assert.match(result.stderr, /cannot be combined with file paths/)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test("report a missing file", () => {
|
|
137
|
+
const result = runCli([path.join(directory, "missing.css")])
|
|
138
|
+
|
|
139
|
+
assert.equal(result.status, 1)
|
|
140
|
+
assert.match(result.stderr, /Cannot access/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("reject a directory", () => {
|
|
144
|
+
const result = runCli([directory])
|
|
145
|
+
|
|
146
|
+
assert.equal(result.status, 1)
|
|
147
|
+
assert.match(result.stderr, /is not a file/)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("report a transformation error", async () => {
|
|
151
|
+
const result = runCli([await writeFile("invalid.css", deepAtRuleCss)])
|
|
152
|
+
|
|
153
|
+
assert.equal(result.status, 128)
|
|
154
|
+
assert.match(result.stderr, /Error:/)
|
|
155
|
+
assert.match(result.stdout, /1 file failed to process/)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test("print an error stack in verbose mode", async () => {
|
|
159
|
+
const result = runCli(["--verbose", await writeFile("verbose.css", deepAtRuleCss)])
|
|
160
|
+
|
|
161
|
+
assert.equal(result.status, 128)
|
|
162
|
+
assert.match(result.stderr, /RangeError/)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test("count multiple files that need upgrading in check mode", async () => {
|
|
166
|
+
const result = runCli([
|
|
167
|
+
"--check",
|
|
168
|
+
await writeFile("check-one.css", ".a { color: red } .a .b { color: blue }"),
|
|
169
|
+
await writeFile("check-two.css", ".c { color: red } .c .d { color: blue }"),
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
assert.equal(result.status, 1)
|
|
173
|
+
assert.match(result.stdout, /2 files need upgrading/)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("count multiple modified files", async () => {
|
|
177
|
+
const result = runCli([
|
|
178
|
+
await writeFile("one.css", ".a { color: red } .a .b { color: blue }"),
|
|
179
|
+
await writeFile("two.css", ".c { color: red } .c .d { color: blue }"),
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
assert.equal(result.status, 0)
|
|
183
|
+
assert.match(result.stdout, /2 files would be upgraded/)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe("cssupgrade CLI stdin errors", () => {
|
|
188
|
+
test("report a transformation error from stdin", () => {
|
|
189
|
+
const result = runCli(["-"], deepAtRuleCss)
|
|
190
|
+
|
|
191
|
+
assert.equal(result.status, 128)
|
|
192
|
+
assert.match(result.stderr, /Error: stdin:/)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("print an error stack from stdin in verbose mode", () => {
|
|
196
|
+
const result = runCli(["--verbose", "-"], deepAtRuleCss)
|
|
197
|
+
|
|
198
|
+
assert.equal(result.status, 128)
|
|
199
|
+
assert.match(result.stderr, /RangeError/)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test("reject more than one stdin argument", () => {
|
|
203
|
+
const result = runCli(["-", "-"], "")
|
|
204
|
+
|
|
205
|
+
assert.equal(result.status, 1)
|
|
206
|
+
assert.match(result.stderr, /cannot be combined with file paths/)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe("cssupgrade CLI write errors", () => {
|
|
211
|
+
test("report a file that cannot be written", async () => {
|
|
212
|
+
const writeDirectory = await fs.mkdtemp(path.join(tmpdir(), "cssupgrade-write-"))
|
|
213
|
+
const filePath = path.join(writeDirectory, "read-only.css")
|
|
214
|
+
|
|
215
|
+
await fs.writeFile(filePath, ".a { color: red } .a .b { color: blue }", "utf8")
|
|
216
|
+
await fs.chmod(filePath, 0o444)
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const result = runCli(["--write", filePath])
|
|
220
|
+
|
|
221
|
+
assert.equal(result.status, 128)
|
|
222
|
+
assert.match(result.stderr, /Error:/)
|
|
223
|
+
assert.match(result.stdout, /1 file failed to process/)
|
|
224
|
+
} finally {
|
|
225
|
+
await fs.chmod(filePath, 0o644)
|
|
226
|
+
await fs.rm(writeDirectory, { recursive: true, force: true })
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
})
|