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/src/pool.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import { Worker } from "worker_threads"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {(filePath: string, options: Object, worker: TransformWorker) => Promise<{modified: boolean, error: boolean}>} FileProcessorFunction
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Restore an error posted by a transform worker.
|
|
10
|
+
*
|
|
11
|
+
* @param {{name: string, message: string, stack: string | undefined}} errorData - Plain error data.
|
|
12
|
+
* @returns {Error} Error carrying the worker name, message, and stack.
|
|
13
|
+
*/
|
|
14
|
+
function reviveError({ name, message, stack }) {
|
|
15
|
+
return Object.assign(new Error(message), { name, stack })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loading the transformation pipeline takes tens of milliseconds, so one worker
|
|
20
|
+
* transforms many files instead of being spawned for a single file.
|
|
21
|
+
*/
|
|
22
|
+
export class TransformWorker {
|
|
23
|
+
#worker
|
|
24
|
+
#pending = null
|
|
25
|
+
#stopError = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string | URL} workerPath - Path of the worker thread module
|
|
29
|
+
*/
|
|
30
|
+
constructor(workerPath) {
|
|
31
|
+
this.#worker = new Worker(workerPath)
|
|
32
|
+
this.#worker.on("message", (message) => this.#settle(message))
|
|
33
|
+
this.#worker.on("error", (error) => this.#stop(error))
|
|
34
|
+
this.#worker.on("exit", (code) =>
|
|
35
|
+
this.#stop(new Error(`Worker stopped with exit code ${code}`)),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get stopped() {
|
|
40
|
+
return this.#stopError !== null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A stopped thread drops posted messages without a response, so requests fail
|
|
45
|
+
* immediately instead of waiting for a reply that never comes. One request runs
|
|
46
|
+
* at a time, so a second request fails loudly instead of losing the first.
|
|
47
|
+
*
|
|
48
|
+
* @param {{filePath: string, baseline: string, includeOriginal: boolean}} request - File to transform
|
|
49
|
+
* @returns {Promise<Object>} Worker response message
|
|
50
|
+
*/
|
|
51
|
+
transform(request) {
|
|
52
|
+
if (this.#stopError) {
|
|
53
|
+
return Promise.reject(this.#stopError)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (this.#pending) {
|
|
57
|
+
return Promise.reject(new Error("Worker already transformed a file"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.#pending = { resolve, reject }
|
|
62
|
+
this.#worker.postMessage(request)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @returns {Promise<number>} Exit code of the stopped thread
|
|
68
|
+
*/
|
|
69
|
+
terminate() {
|
|
70
|
+
return this.#worker.terminate()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {Object} message - Worker response message
|
|
75
|
+
*/
|
|
76
|
+
#settle(message) {
|
|
77
|
+
this.#pending?.resolve(
|
|
78
|
+
message.success ? message : { ...message, error: reviveError(message.error) },
|
|
79
|
+
)
|
|
80
|
+
this.#pending = null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {Error} error - Reason the worker stopped
|
|
85
|
+
*/
|
|
86
|
+
#stop(error) {
|
|
87
|
+
this.#stopError ??= error
|
|
88
|
+
this.#pending?.reject(error)
|
|
89
|
+
this.#pending = null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Each worker loads the transformation pipeline once and then processes files
|
|
95
|
+
* until the queue is empty. A worker that stopped is replaced before it takes on
|
|
96
|
+
* the next file, so one stopped thread cannot fail the remaining files.
|
|
97
|
+
*/
|
|
98
|
+
export class WorkerPool {
|
|
99
|
+
#fileProcessor
|
|
100
|
+
#workerPath
|
|
101
|
+
#maxWorkers
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {FileProcessorFunction} fileProcessor - Processor that handles one file
|
|
105
|
+
* @param {string | URL} workerPath - Path of the worker thread module
|
|
106
|
+
* @param {number} [maxWorkers] - Upper bound of concurrent workers
|
|
107
|
+
*/
|
|
108
|
+
constructor(fileProcessor, workerPath, maxWorkers = os.availableParallelism()) {
|
|
109
|
+
this.#fileProcessor = fileProcessor
|
|
110
|
+
this.#workerPath = workerPath
|
|
111
|
+
this.#maxWorkers = maxWorkers
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The results keep the input order of the given files.
|
|
116
|
+
*
|
|
117
|
+
* @param {string[]} files - Files to process
|
|
118
|
+
* @param {Object} options - Processing options
|
|
119
|
+
* @returns {Promise<Array<{modified: boolean, error: boolean}>>} Result per file
|
|
120
|
+
*/
|
|
121
|
+
async processFiles(files, options) {
|
|
122
|
+
const results = new Array(files.length)
|
|
123
|
+
let fileIndex = 0
|
|
124
|
+
|
|
125
|
+
const processQueue = async () => {
|
|
126
|
+
let worker = new TransformWorker(this.#workerPath)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
while (fileIndex < files.length) {
|
|
130
|
+
const currentIndex = fileIndex++
|
|
131
|
+
results[currentIndex] = await this.#fileProcessor(
|
|
132
|
+
files[currentIndex],
|
|
133
|
+
options,
|
|
134
|
+
worker,
|
|
135
|
+
)
|
|
136
|
+
worker = await this.#replaceStoppedWorker(worker)
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
await worker.terminate()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const workerCount = Math.min(this.#maxWorkers, files.length)
|
|
144
|
+
const queues = Array.from({ length: workerCount }, processQueue)
|
|
145
|
+
await Promise.all(queues)
|
|
146
|
+
|
|
147
|
+
return results
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Provide a running worker for the next file.
|
|
152
|
+
*
|
|
153
|
+
* @param {TransformWorker} worker - Worker that handled the previous file
|
|
154
|
+
* @returns {Promise<TransformWorker>} The worker itself, or a replacement
|
|
155
|
+
*/
|
|
156
|
+
async #replaceStoppedWorker(worker) {
|
|
157
|
+
if (!worker.stopped) {
|
|
158
|
+
return worker
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await worker.terminate()
|
|
162
|
+
|
|
163
|
+
return new TransformWorker(this.#workerPath)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source text conditions that decide whether a transformer can match.
|
|
3
|
+
*
|
|
4
|
+
* Every transformer parses the whole source, which dominates the runtime of
|
|
5
|
+
* this package. A transformer can only match when its pattern occurs in the
|
|
6
|
+
* source text, so testing the text first skips the parse for transformations
|
|
7
|
+
* that cannot apply.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Conditions a transformer needs in the source text. A transformer runs when all
|
|
12
|
+
* of its conditions match. Each condition must be necessary: when the source
|
|
13
|
+
* text fails a condition, the transformer cannot change the tree.
|
|
14
|
+
*
|
|
15
|
+
* @type {Map<string, RegExp[]>}
|
|
16
|
+
*/
|
|
17
|
+
export const prefilters = new Map([
|
|
18
|
+
// Nesting needs two rules, so the source holds at least two blocks.
|
|
19
|
+
["cssNesting", [/\{[\s\S]*\{/]],
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} name - Transformer name
|
|
24
|
+
* @param {string} code - Source text of the current transformation pass
|
|
25
|
+
* @returns {boolean} True when the transformer needs to run
|
|
26
|
+
*/
|
|
27
|
+
export function matchesPrefilter(name, code) {
|
|
28
|
+
return prefilters.get(name).every((condition) => condition.test(code))
|
|
29
|
+
}
|