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/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
+ }