cssupgrade 0.0.0-stage → 2026.0.1

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.
@@ -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.1",
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
+ }
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
+ }