@castlenine/vite-remove-attribute 1.0.1 → 2.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/dist/index.cjs ADDED
@@ -0,0 +1,540 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ let node_path = require("node:path");
6
+ //#region src/sourcemap.ts
7
+ var BASE64_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
8
+ var WORD_CHARACTER_REGEX = /\w/;
9
+ /**
10
+ * Encodes a single integer value as a base64 VLQ (Variable Length Quantity) string.
11
+ *
12
+ * @remarks
13
+ * Base64 VLQ is the encoding format used for numbers in source map `mappings` fields.
14
+ * This function encodes both positive and negative integers using bitwise operations,
15
+ * setting the least significant bit for sign and using continuation bits for multi-digit numbers.
16
+ *
17
+ * @param value - The integer to encode. Can be positive, negative, or zero.
18
+ *
19
+ * @returns The VLQ Base64-encoded string representation.
20
+ *
21
+ * @see [Source Map V3 Spec](https://sourcemaps.info/spec.html)
22
+ */
23
+ function encodeVlq(value) {
24
+ let vlq = value < 0 ? -value << 1 | 1 : value << 1;
25
+ let output = "";
26
+ do {
27
+ let digit = vlq & 31;
28
+ vlq >>>= 5;
29
+ if (vlq > 0) digit |= 32;
30
+ output += BASE64_CHARACTERS.charAt(digit);
31
+ } while (vlq > 0);
32
+ return output;
33
+ }
34
+ /**
35
+ * Builds the `mappings` string one generated line at a time, encoding every segment as deltas from the previous one
36
+ */
37
+ var MappingsWriter = class {
38
+ lines = [];
39
+ segments = [];
40
+ lastGeneratedColumn = 0;
41
+ lastOriginalLine = 0;
42
+ lastOriginalColumn = 0;
43
+ lastMappedColumn = -1;
44
+ addSegment(generatedColumn, originalLine, originalColumn) {
45
+ if (generatedColumn === this.lastMappedColumn) return;
46
+ this.segments.push(encodeVlq(generatedColumn - this.lastGeneratedColumn) + encodeVlq(0) + encodeVlq(originalLine - this.lastOriginalLine) + encodeVlq(originalColumn - this.lastOriginalColumn));
47
+ this.lastGeneratedColumn = generatedColumn;
48
+ this.lastOriginalLine = originalLine;
49
+ this.lastOriginalColumn = originalColumn;
50
+ this.lastMappedColumn = generatedColumn;
51
+ }
52
+ endLine() {
53
+ this.lines.push(this.segments.join(","));
54
+ this.segments = [];
55
+ this.lastGeneratedColumn = 0;
56
+ this.lastMappedColumn = -1;
57
+ }
58
+ toString() {
59
+ this.endLine();
60
+ return this.lines.join(";");
61
+ }
62
+ };
63
+ /**
64
+ * Generates a source map for a given `source` string with the specified ranges removed.
65
+ *
66
+ * Emits a segment at the start of each generated line, at each position where the original text was cut, and at
67
+ * every word boundary in between, allowing consumers to resolve any token to its original line and column.
68
+ *
69
+ * @param source - The original source code string.
70
+ * @param ranges - Sorted, non-overlapping `[start, end)` character ranges to be removed.
71
+ * @param file - The original file name or path. Used as the single source entry in the resulting source map.
72
+ *
73
+ * @returns The generated SourceMap object with correct mappings after range removal.
74
+ */
75
+ function generateRemovalSourceMap(source, ranges, file) {
76
+ const WRITER = new MappingsWriter();
77
+ let generatedColumn = 0;
78
+ let originalLine = 0;
79
+ let originalColumn = 0;
80
+ let previousCharacter = "";
81
+ let cursor = 0;
82
+ /**
83
+ * Advances the original position from `from` to `to`, updating `originalLine` and `originalColumn`
84
+ * to correctly reflect skipping a removed range.
85
+ *
86
+ * @param from - Start index in the original source.
87
+ * @param to - End index in the original source.
88
+ */
89
+ const ADVANCE_ORIGINAL = (from, to) => {
90
+ for (let index = from; index < to; index++) if (source.charAt(index) === "\n") {
91
+ originalLine++;
92
+ originalColumn = 0;
93
+ } else originalColumn++;
94
+ };
95
+ /**
96
+ * Emits source map segments for the kept region from `from` to `to`.
97
+ *
98
+ * @remarks
99
+ * A segment is written at every new line and at every word boundary change (as determined by
100
+ * `WORD_CHARACTER_REGEX`), so tools consuming the source map can resolve the original location of any token in
101
+ * the generated content. The `generatedColumn`, `originalLine` and `originalColumn` counters are advanced as the
102
+ * region is traversed, and `needsSegment` forces a segment right after a cut.
103
+ *
104
+ * @param from - Start index of the kept region.
105
+ * @param to - End index of the kept region.
106
+ */
107
+ const WRITE_KEPT = (from, to) => {
108
+ let needsSegment = from < to;
109
+ for (let index = from; index < to; index++) {
110
+ const CHARACTER = source.charAt(index);
111
+ if (CHARACTER === "\n") {
112
+ WRITER.endLine();
113
+ generatedColumn = 0;
114
+ originalLine++;
115
+ originalColumn = 0;
116
+ needsSegment = true;
117
+ } else {
118
+ if (needsSegment || WORD_CHARACTER_REGEX.test(CHARACTER) !== WORD_CHARACTER_REGEX.test(previousCharacter)) {
119
+ WRITER.addSegment(generatedColumn, originalLine, originalColumn);
120
+ needsSegment = false;
121
+ }
122
+ generatedColumn++;
123
+ originalColumn++;
124
+ }
125
+ previousCharacter = CHARACTER;
126
+ }
127
+ };
128
+ for (const [START, END] of ranges) {
129
+ WRITE_KEPT(cursor, START);
130
+ ADVANCE_ORIGINAL(START, END);
131
+ cursor = END;
132
+ }
133
+ WRITE_KEPT(cursor, source.length);
134
+ return {
135
+ version: 3,
136
+ sources: [file],
137
+ sourcesContent: [source],
138
+ names: [],
139
+ mappings: WRITER.toString()
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region src/utilities.ts
144
+ var DEFAULT_IGNORE_PATHS = [
145
+ "node_modules",
146
+ ".git",
147
+ ".idea",
148
+ ".vscode",
149
+ ".DS_Store",
150
+ "Thumbs.db",
151
+ ".env",
152
+ ".env.*",
153
+ "logs",
154
+ "*.log",
155
+ "public",
156
+ "build",
157
+ ".svelte-kit",
158
+ "dist",
159
+ ".nuxt",
160
+ ".next",
161
+ ".remix",
162
+ "e2e",
163
+ "angular.json",
164
+ "browserslist",
165
+ ".cache"
166
+ ];
167
+ var REGEX_SPECIAL_CHARACTERS_REGEX = /[.*+?^${}()|[\]\\]/g;
168
+ var LEADING_RELATIVE_PREFIX_REGEX = /^(?:\.\/|\/)+/;
169
+ var TRAILING_SLASHES_REGEX = /\/+$/;
170
+ var LEADING_PARENT_SEGMENTS_REGEX = /^(?:\.\.\/)+/;
171
+ var LEADING_DOTS_REGEX = /^\.+/;
172
+ var TOKEN_REGEX_CACHE = /* @__PURE__ */ new Map();
173
+ var EXTENSION_REGEX_CACHE = /* @__PURE__ */ new Map();
174
+ function escapeRegExp(value) {
175
+ return value.replace(REGEX_SPECIAL_CHARACTERS_REGEX, "\\$&");
176
+ }
177
+ function getOptions(options) {
178
+ return {
179
+ extensions: Array.isArray(options.extensions) ? options.extensions : [],
180
+ attributes: Array.isArray(options.attributes) ? options.attributes : [],
181
+ ignoreFolders: Array.isArray(options.ignoreFolders) ? options.ignoreFolders : [],
182
+ ignoreFiles: Array.isArray(options.ignoreFiles) ? options.ignoreFiles : [],
183
+ ignoreDefaults: options.ignoreDefaults !== false
184
+ };
185
+ }
186
+ /**
187
+ * Normalizes a user-supplied ignore token by trimming whitespace,
188
+ * removing leading './' or '/', and stripping trailing '/' characters.
189
+ *
190
+ * @param path - The ignore token to normalize.
191
+ *
192
+ * @returns The normalized ignore token.
193
+ */
194
+ function cleanIgnoredPath(path) {
195
+ return path.trim().replace(LEADING_RELATIVE_PREFIX_REGEX, "").replace(TRAILING_SLASHES_REGEX, "");
196
+ }
197
+ /**
198
+ * Normalizes and de-duplicates ignore tokens by:
199
+ * - Trimming whitespace from each path
200
+ * - Removing leading './' or '/', and trailing '/' characters
201
+ * - Dropping empty and root-only entries (such as `''`, `.`, `/`, and `./`)
202
+ * - Removing duplicate entries in the result
203
+ *
204
+ * @param paths - An array of ignore path tokens to be cleaned and de-duplicated.
205
+ *
206
+ * @returns A new array containing unique, cleaned ignore tokens, with all empty or root-only entries omitted.
207
+ */
208
+ function cleanIgnoredPaths(paths) {
209
+ const CLEANED = paths.filter((path) => typeof path === "string").map(cleanIgnoredPath).filter((path) => path !== "" && path !== ".");
210
+ return [...new Set(CLEANED)];
211
+ }
212
+ /**
213
+ * Returns the combined and cleaned list of ignored paths based on the provided {@link ResolvedOptions}.
214
+ *
215
+ * @remarks
216
+ * - Merges `ignoreFolders` and `ignoreFiles` into a single list.
217
+ * - Cleans each entry using {@link cleanIgnoredPaths}.
218
+ * - If `ignoreDefaults` is not explicitly set to `false`, the list also includes {@link DEFAULT_IGNORE_PATHS}.
219
+ * - The resulting list is de-duplicated.
220
+ *
221
+ * @param options - The resolved options containing `ignoreFolders`, `ignoreFiles` and the `ignoreDefaults` flag.
222
+ *
223
+ * @returns An array of unique, cleaned ignore path tokens, including the built-in defaults unless
224
+ * {@link ResolvedOptions.ignoreDefaults} is `false`.
225
+ */
226
+ function getIgnoredPaths(options) {
227
+ const CONFIGURED = cleanIgnoredPaths([...options.ignoreFolders, ...options.ignoreFiles]);
228
+ return options.ignoreDefaults ? [.../* @__PURE__ */ new Set([...CONFIGURED, ...DEFAULT_IGNORE_PATHS])] : CONFIGURED;
229
+ }
230
+ /**
231
+ * Removes the `?query` suffix that Vite appends to module IDs.
232
+ *
233
+ * For example, given `/src/App.svelte?svelte&type=style&lang.css`, this function will return `/src/App.svelte`.
234
+ *
235
+ * @param id - The module ID which may include a `?query` suffix.
236
+ *
237
+ * @returns The module ID without any `?query` part.
238
+ */
239
+ function stripQuery(id) {
240
+ const QUERY_INDEX = id.indexOf("?");
241
+ return QUERY_INDEX === -1 ? id : id.slice(0, QUERY_INDEX);
242
+ }
243
+ /**
244
+ * Returns the path of a module ID relative to the Vite root directory, using POSIX separators.
245
+ *
246
+ * @param id - The module ID to be converted to a relative path.
247
+ * @param root - The root directory to which the path will be made relative.
248
+ *
249
+ * @returns The relative path from the root to the module ID, using '/' as the separator.
250
+ */
251
+ function toRelativePath(id, root) {
252
+ return (0, node_path.relative)(root, stripQuery(id)).split(node_path.sep).join("/");
253
+ }
254
+ /**
255
+ * Returns a cached or newly-created regular expression to match a path segment token,
256
+ * supporting `*` wildcards matching within a segment (but not across path separators).
257
+ *
258
+ * The regular expression is built so that `*` matches any sequence of characters except '/'.
259
+ * The regex is cached for subsequent calls with the same token.
260
+ *
261
+ * @param token - The ignore token, possibly containing `*` wildcards.
262
+ *
263
+ * @returns The regular expression corresponding to the token, matching segment boundaries.
264
+ */
265
+ function getTokenRegex(token) {
266
+ const CACHED = TOKEN_REGEX_CACHE.get(token);
267
+ if (CACHED) return CACHED;
268
+ const PATTERN = token.split("*").map(escapeRegExp).join("[^/]*");
269
+ const REGEX = new RegExp(`(?:^|/)${PATTERN}(?:/|$)`);
270
+ TOKEN_REGEX_CACHE.set(token, REGEX);
271
+ return REGEX;
272
+ }
273
+ /**
274
+ * Checks if the given relative path matches any of the provided ignore tokens,
275
+ * comparing on path-segment boundaries. Ignores leading parent directory segments.
276
+ *
277
+ * @remarks
278
+ * This function ignores absolute IDs to avoid false positives from folder names that coincidentally
279
+ * contain ignore tokens. For example, in environments like Cloudflare where a repo may be cloned
280
+ * to a directory such as `/opt/buildhome/repo`, a token like `build` should not match simply
281
+ * because it's part of the parent path. Only the path relative to the Vite root is considered.
282
+ *
283
+ * Leading `../` segments are stripped from the path so that modules resolved outside the root
284
+ * (e.g. `../../.pnpm/x/node_modules/y/index.js`) still have the opportunity to match ignore tokens
285
+ * like `node_modules` against their segments.
286
+ *
287
+ * @param relativePath - The path of the module relative to the Vite root.
288
+ * @param tokens - The list of ignore tokens, possibly including `*` wildcards.
289
+ *
290
+ * @returns `true` if the relative path matches any ignore token; otherwise, `false`.
291
+ */
292
+ function hasIgnorePath(relativePath, tokens) {
293
+ const PATH = relativePath.replace(LEADING_PARENT_SEGMENTS_REGEX, "");
294
+ return tokens.some((token) => getTokenRegex(token).test(PATH));
295
+ }
296
+ /**
297
+ * Returns a regular expression that matches any of the provided file extensions at the end of a string.
298
+ *
299
+ * The extensions are matched case-insensitively and can be provided with or without leading dots.
300
+ * The resulting regex is cached for subsequent calls with the same set of extensions.
301
+ *
302
+ * @param extensions - An array of file extension strings (with or without leading dots).
303
+ *
304
+ * @returns A RegExp instance matching any of the specified extensions as a file suffix.
305
+ */
306
+ function getExtensionRegex(extensions) {
307
+ const KEY = extensions.join("|");
308
+ const CACHED = EXTENSION_REGEX_CACHE.get(KEY);
309
+ if (CACHED) return CACHED;
310
+ const PATTERN = extensions.map((extension) => escapeRegExp(extension.replace(LEADING_DOTS_REGEX, ""))).join("|");
311
+ const REGEX = new RegExp(`\\.(?:${PATTERN})$`, "i");
312
+ EXTENSION_REGEX_CACHE.set(KEY, REGEX);
313
+ return REGEX;
314
+ }
315
+ /**
316
+ * Determines whether the provided module id (with query suffix stripped) ends with one of the specified extensions.
317
+ *
318
+ * @param id - The module identifier, possibly including a query suffix (e.g., `file.js?raw`).
319
+ * @param extensions - An array of file extension strings (with or without leading dots).
320
+ *
321
+ * @returns `true` if the module id (excluding the query suffix) ends with one of the provided extensions;
322
+ * otherwise, `false`.
323
+ */
324
+ function hasExtension(id, extensions) {
325
+ if (extensions.length === 0) return false;
326
+ return getExtensionRegex(extensions).test(stripQuery(id));
327
+ }
328
+ /**
329
+ * Character-by-character scanner for an `={…}` expression: tracks nested braces, quoted strings, template literals
330
+ * and their `${…}` placeholders, and reports when the opening brace is closed
331
+ */
332
+ var ExpressionScanner = class {
333
+ frames = [{
334
+ kind: "expression",
335
+ depth: 1
336
+ }];
337
+ stringQuote = "";
338
+ isEscaped = false;
339
+ step(character, next) {
340
+ if (this.isEscaped) {
341
+ this.isEscaped = false;
342
+ return "continue";
343
+ }
344
+ if (character === "\\") {
345
+ this.isEscaped = true;
346
+ return "continue";
347
+ }
348
+ if (this.stringQuote !== "") {
349
+ if (character === this.stringQuote) this.stringQuote = "";
350
+ return "continue";
351
+ }
352
+ const FRAME = this.frames.at(-1);
353
+ if (!FRAME) return "continue";
354
+ return FRAME.kind === "template" ? this.stepTemplate(character, next) : this.stepExpression(FRAME, character);
355
+ }
356
+ stepTemplate(character, next) {
357
+ if (character === "`") this.frames.pop();
358
+ else if (character === "$" && next === "{") {
359
+ this.frames.push({
360
+ kind: "expression",
361
+ depth: 1
362
+ });
363
+ return "skip-next";
364
+ }
365
+ return "continue";
366
+ }
367
+ stepExpression(frame, character) {
368
+ if (character === "'" || character === "\"") this.stringQuote = character;
369
+ else if (character === "`") this.frames.push({ kind: "template" });
370
+ else if (character === "{") frame.depth++;
371
+ else if (character === "}") return this.closeBrace(frame);
372
+ return "continue";
373
+ }
374
+ closeBrace(frame) {
375
+ frame.depth--;
376
+ if (frame.depth > 0) return "continue";
377
+ if (this.frames.length === 1) return "closed";
378
+ this.frames.pop();
379
+ return "continue";
380
+ }
381
+ };
382
+ /**
383
+ * Finds the closing brace for an `={…}` attribute value in a string of markup. Returns the index of the closing
384
+ * brace, or `-1` if the braces are unbalanced or not found.
385
+ *
386
+ * @remarks
387
+ * A regular expression cannot do this: the expression may hold a template literal whose `${…}` placeholders nest
388
+ * further braces, and a brace inside a string is text rather than structure. The scanner tracks both.
389
+ *
390
+ * @param input - The input string containing the markup or code.
391
+ * @param openingBraceIndex - The index of the opening `{` character to start scanning from.
392
+ *
393
+ * @returns The index of the corresponding closing `}` brace, or `-1` if unmatched.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * findExpressionEnd('foo={a + {b: `1${two}`}}', 4); // returns the index of the matching }
398
+ * ```
399
+ */
400
+ function findExpressionEnd(input, openingBraceIndex) {
401
+ const SCANNER = new ExpressionScanner();
402
+ for (let index = openingBraceIndex + 1; index < input.length; index++) {
403
+ const STEP = SCANNER.step(input.charAt(index), input.charAt(index + 1));
404
+ if (STEP === "closed") return index;
405
+ if (STEP === "skip-next") index++;
406
+ }
407
+ return -1;
408
+ }
409
+ /**
410
+ * Finds all ranges (start and end indices) for a specific attribute in the given markup input.
411
+ *
412
+ * Handles quoted values, expression values (i.e., `={...}`), and bare attributes, including optional
413
+ * `:` or `v-bind:` prefixes. The detection accounts for whitespace before the attribute, as well as attributes
414
+ * written on their own line. If an attribute value is an expression (`={...}`), this function uses
415
+ * {@link findExpressionEnd} to locate the closing brace, and gracefully skips unbalanced or invalid expressions.
416
+ *
417
+ * @remarks
418
+ * The returned ranges are suitable for text replacements: all whitespace before the attribute is included
419
+ * so that removing the attribute does not leave a dangling blank line or extra spaces.
420
+ *
421
+ * @param input - The input markup string to search for the attribute within.
422
+ * @param attribute - The name of the attribute to locate (e.g., `"data-testid"`).
423
+ *
424
+ * @returns An array of `[start, end)` ranges (as type `Range`) for each found occurrence of the attribute.
425
+ */
426
+ function findAttributeRangesFor(input, attribute) {
427
+ const NAME = escapeRegExp(attribute);
428
+ const PATTERN = new RegExp(`\\s+(?::|v-bind:)?${NAME}(?:\\s*=\\s*(?:(['"\`])(?:(?!\\1)[\\s\\S])*\\1|(\\{))|(?=[\\s/>]))`, "gi");
429
+ const RANGES = [];
430
+ let match = PATTERN.exec(input);
431
+ while (match !== null) {
432
+ const MATCH_END = match.index + match[0].length;
433
+ let attributeEnd = MATCH_END;
434
+ if (match[2] !== void 0) {
435
+ const CLOSING_BRACE_INDEX = findExpressionEnd(input, MATCH_END - 1);
436
+ if (CLOSING_BRACE_INDEX === -1) {
437
+ match = PATTERN.exec(input);
438
+ continue;
439
+ }
440
+ attributeEnd = CLOSING_BRACE_INDEX + 1;
441
+ }
442
+ RANGES.push([match.index, attributeEnd]);
443
+ PATTERN.lastIndex = attributeEnd;
444
+ match = PATTERN.exec(input);
445
+ }
446
+ return RANGES;
447
+ }
448
+ /**
449
+ * Returns the merged ranges of every occurrence of the given attributes (in quoted, `={expression}`,
450
+ * or bare form, with optional `:` or `v-bind:` prefix) within the markup string. The resulting
451
+ * ranges are sorted by start index, and any touching or overlapping ranges are merged.
452
+ *
453
+ * All attributes are located in the same original input, so the returned ranges can be used
454
+ * for sourcemap-aware or batch editing operations.
455
+ *
456
+ * @param input - The markup string to search for the attributes.
457
+ * @param attributes - An array of attribute names to search for (e.g., `['data-testid', 'data-id']`).
458
+ *
459
+ * @returns An array of `Range` tuples, each representing [start, end) of a matched attribute occurrence,
460
+ * with adjacent and overlapping ranges merged.
461
+ */
462
+ function findAttributeRanges(input, attributes) {
463
+ const RANGES = attributes.flatMap((attribute) => findAttributeRangesFor(input, attribute)).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
464
+ const MERGED = [];
465
+ for (const [START, END] of RANGES) {
466
+ const LAST = MERGED.at(-1);
467
+ if (LAST && START <= LAST[1]) LAST[1] = Math.max(LAST[1], END);
468
+ else MERGED.push([START, END]);
469
+ }
470
+ return MERGED;
471
+ }
472
+ /**
473
+ * Returns a copy of the input string with the specified ranges removed.
474
+ *
475
+ * The provided ranges must be sorted by start index and non-overlapping.
476
+ * Each range is specified as a tuple [start, end), with `start` inclusive and `end` exclusive.
477
+ * The function concatenates the segments of the input string outside of these ranges.
478
+ *
479
+ * @param input - The original string from which to remove segments.
480
+ * @param ranges - An array of `[start, end)` index ranges (sorted, non-overlapping) to cut out from the input.
481
+ *
482
+ * @returns The resulting string after all specified ranges have been removed from the input.
483
+ */
484
+ function removeRanges(input, ranges) {
485
+ if (ranges.length === 0) return input;
486
+ const SEGMENTS = [];
487
+ let lastEnd = 0;
488
+ for (const [START, END] of ranges) {
489
+ SEGMENTS.push(input.slice(lastEnd, START));
490
+ lastEnd = END;
491
+ }
492
+ SEGMENTS.push(input.slice(lastEnd));
493
+ return SEGMENTS.join("");
494
+ }
495
+ //#endregion
496
+ //#region src/index.ts
497
+ /**
498
+ * Vite plugin to remove specified attributes from markup files.
499
+ *
500
+ * This plugin scans files with configured extensions and removes
501
+ * attributes as defined in the option list. It also produces
502
+ * accurate source maps to ensure the original line and column numbers
503
+ * are preserved for consumers of the Vite build chain.
504
+ *
505
+ * Files can be ignored based on path token matching. Ignore patterns
506
+ * are resolved relative to the Vite root.
507
+ *
508
+ * @param options - Plugin configuration including which attributes
509
+ * and file extensions to target, and optional ignore paths.
510
+ *
511
+ * @returns A Vite plugin object that removes attributes during
512
+ * transformation steps.
513
+ */
514
+ function removeAttributesPlugin(options) {
515
+ const OPTIONS = getOptions(options);
516
+ const IGNORED_PATHS = getIgnoredPaths(OPTIONS);
517
+ let root = process.cwd();
518
+ return {
519
+ name: "remove-attributes",
520
+ enforce: "pre",
521
+ configResolved(config) {
522
+ root = config.root;
523
+ },
524
+ transform(code, id) {
525
+ if (id.startsWith("\0") || !hasExtension(id, OPTIONS.extensions) || hasIgnorePath(toRelativePath(id, root), IGNORED_PATHS)) return null;
526
+ const RANGES = findAttributeRanges(code, OPTIONS.attributes);
527
+ if (RANGES.length === 0) return null;
528
+ return {
529
+ code: removeRanges(code, RANGES),
530
+ map: generateRemovalSourceMap(code, RANGES, stripQuery(id))
531
+ };
532
+ }
533
+ };
534
+ }
535
+ //#endregion
536
+ exports.default = removeAttributesPlugin;
537
+ module.exports = exports.default;
538
+ module.exports.default = module.exports;
539
+
540
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/sourcemap.ts","../src/utilities.ts","../src/index.ts"],"sourcesContent":["import type { Range } from './utilities';\n\n/**\n * Source map v3 object, as accepted by Vite's `transform` hook (`map`)\n */\ninterface SourceMap {\n\tversion: 3;\n\tsources: string[];\n\tsourcesContent: string[];\n\tnames: string[];\n\tmappings: string;\n}\n\nconst BASE64_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst WORD_CHARACTER_REGEX = /\\w/;\n\n/**\n * Encodes a single integer value as a base64 VLQ (Variable Length Quantity) string.\n *\n * @remarks\n * Base64 VLQ is the encoding format used for numbers in source map `mappings` fields.\n * This function encodes both positive and negative integers using bitwise operations,\n * setting the least significant bit for sign and using continuation bits for multi-digit numbers.\n *\n * @param value - The integer to encode. Can be positive, negative, or zero.\n *\n * @returns The VLQ Base64-encoded string representation.\n *\n * @see [Source Map V3 Spec](https://sourcemaps.info/spec.html)\n */\nfunction encodeVlq(value: number): string {\n\tlet vlq = value < 0 ? (-value << 1) | 1 : value << 1;\n\tlet output = '';\n\n\tdo {\n\t\tlet digit = vlq & 31;\n\n\t\tvlq >>>= 5;\n\n\t\tif (vlq > 0) {\n\t\t\tdigit |= 32;\n\t\t}\n\n\t\toutput += BASE64_CHARACTERS.charAt(digit);\n\t} while (vlq > 0);\n\n\treturn output;\n}\n\n/**\n * Builds the `mappings` string one generated line at a time, encoding every segment as deltas from the previous one\n */\nclass MappingsWriter {\n\tprivate readonly lines: string[] = [];\n\tprivate segments: string[] = [];\n\tprivate lastGeneratedColumn = 0;\n\tprivate lastOriginalLine = 0;\n\tprivate lastOriginalColumn = 0;\n\tprivate lastMappedColumn = -1;\n\n\taddSegment(generatedColumn: number, originalLine: number, originalColumn: number): void {\n\t\t// Two candidates for the same generated column always describe the same original position\n\t\tif (generatedColumn === this.lastMappedColumn) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.segments.push(\n\t\t\tencodeVlq(generatedColumn - this.lastGeneratedColumn) +\n\t\t\t\tencodeVlq(0) +\n\t\t\t\tencodeVlq(originalLine - this.lastOriginalLine) +\n\t\t\t\tencodeVlq(originalColumn - this.lastOriginalColumn),\n\t\t);\n\t\tthis.lastGeneratedColumn = generatedColumn;\n\t\tthis.lastOriginalLine = originalLine;\n\t\tthis.lastOriginalColumn = originalColumn;\n\t\tthis.lastMappedColumn = generatedColumn;\n\t}\n\n\tendLine(): void {\n\t\tthis.lines.push(this.segments.join(','));\n\t\tthis.segments = [];\n\t\tthis.lastGeneratedColumn = 0;\n\t\tthis.lastMappedColumn = -1;\n\t}\n\n\ttoString(): string {\n\t\tthis.endLine();\n\n\t\treturn this.lines.join(';');\n\t}\n}\n\n/**\n * Generates a source map for a given `source` string with the specified ranges removed.\n *\n * Emits a segment at the start of each generated line, at each position where the original text was cut, and at\n * every word boundary in between, allowing consumers to resolve any token to its original line and column.\n *\n * @param source - The original source code string.\n * @param ranges - Sorted, non-overlapping `[start, end)` character ranges to be removed.\n * @param file - The original file name or path. Used as the single source entry in the resulting source map.\n *\n * @returns The generated SourceMap object with correct mappings after range removal.\n */\nfunction generateRemovalSourceMap(source: string, ranges: Range[], file: string): SourceMap {\n\tconst WRITER = new MappingsWriter();\n\n\tlet generatedColumn = 0;\n\tlet originalLine = 0;\n\tlet originalColumn = 0;\n\tlet previousCharacter = '';\n\tlet cursor = 0;\n\n\t/**\n\t * Advances the original position from `from` to `to`, updating `originalLine` and `originalColumn`\n\t * to correctly reflect skipping a removed range.\n\t *\n\t * @param from - Start index in the original source.\n\t * @param to - End index in the original source.\n\t */\n\tconst ADVANCE_ORIGINAL = (from: number, to: number): void => {\n\t\tfor (let index = from; index < to; index++) {\n\t\t\tif (source.charAt(index) === '\\n') {\n\t\t\t\toriginalLine++;\n\t\t\t\toriginalColumn = 0;\n\t\t\t} else {\n\t\t\t\toriginalColumn++;\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Emits source map segments for the kept region from `from` to `to`.\n\t *\n\t * @remarks\n\t * A segment is written at every new line and at every word boundary change (as determined by\n\t * `WORD_CHARACTER_REGEX`), so tools consuming the source map can resolve the original location of any token in\n\t * the generated content. The `generatedColumn`, `originalLine` and `originalColumn` counters are advanced as the\n\t * region is traversed, and `needsSegment` forces a segment right after a cut.\n\t *\n\t * @param from - Start index of the kept region.\n\t * @param to - End index of the kept region.\n\t */\n\tconst WRITE_KEPT = (from: number, to: number): void => {\n\t\tlet needsSegment = from < to;\n\n\t\tfor (let index = from; index < to; index++) {\n\t\t\tconst CHARACTER = source.charAt(index);\n\n\t\t\tif (CHARACTER === '\\n') {\n\t\t\t\tWRITER.endLine();\n\t\t\t\tgeneratedColumn = 0;\n\t\t\t\toriginalLine++;\n\t\t\t\toriginalColumn = 0;\n\t\t\t\tneedsSegment = true;\n\t\t\t} else {\n\t\t\t\tif (needsSegment || WORD_CHARACTER_REGEX.test(CHARACTER) !== WORD_CHARACTER_REGEX.test(previousCharacter)) {\n\t\t\t\t\tWRITER.addSegment(generatedColumn, originalLine, originalColumn);\n\t\t\t\t\tneedsSegment = false;\n\t\t\t\t}\n\n\t\t\t\tgeneratedColumn++;\n\t\t\t\toriginalColumn++;\n\t\t\t}\n\n\t\t\tpreviousCharacter = CHARACTER;\n\t\t}\n\t};\n\n\t// Emit the kept text before each removed range, then advance the original position through the removed\n\t// content; whatever follows the last range is emitted afterwards\n\tfor (const [START, END] of ranges) {\n\t\tWRITE_KEPT(cursor, START);\n\t\tADVANCE_ORIGINAL(START, END);\n\t\tcursor = END;\n\t}\n\n\tWRITE_KEPT(cursor, source.length);\n\n\treturn {\n\t\tversion: 3,\n\t\tsources: [file],\n\t\tsourcesContent: [source],\n\t\tnames: [],\n\t\tmappings: WRITER.toString(),\n\t};\n}\n\nexport type { SourceMap };\nexport { encodeVlq, generateRemovalSourceMap };\n","import type { Options, ResolvedOptions } from './types';\n\nimport { relative, sep } from 'node:path';\n\nconst DEFAULT_IGNORE_PATHS: string[] = [\n\t// Node modules\n\t'node_modules',\n\n\t// Git\n\t'.git',\n\n\t// IDE configurations\n\t'.idea', // JetBrains IDEs (e.g., WebStorm)\n\t'.vscode', // Visual Studio Code\n\n\t// OS generated files\n\t'.DS_Store', // macOS\n\t'Thumbs.db', // Windows\n\n\t// Environment variables\n\t'.env',\n\t'.env.*', // .env.development, .env.production, etc.\n\n\t// Logs\n\t'logs',\n\t'*.log',\n\n\t// Svelte\n\t'public', // Svelte.js public folder\n\t'build', // Svelte.js build folder\n\n\t// SvelteKit\n\t'.svelte-kit', // SvelteKit generates this folder\n\n\t// Dist\n\t'dist', // Distribution folder\n\n\t// Vue.js\n\t'.nuxt', // Nuxt.js generates this folder\n\n\t// React.js\n\t'.next', // Next.js generates this folder\n\n\t// Remix.js\n\t'.remix', // Remix.js cache\n\n\t// Angular\n\t'e2e', // End-to-end tests in Angular\n\t'angular.json', // Angular CLI configuration\n\t'browserslist', // Browser compatibility list for Angular\n\n\t'.cache', // Cache files for various tools\n];\n\nconst REGEX_SPECIAL_CHARACTERS_REGEX = /[.*+?^${}()|[\\]\\\\]/g;\nconst LEADING_RELATIVE_PREFIX_REGEX = /^(?:\\.\\/|\\/)+/;\nconst TRAILING_SLASHES_REGEX = /\\/+$/;\nconst LEADING_PARENT_SEGMENTS_REGEX = /^(?:\\.\\.\\/)+/;\nconst LEADING_DOTS_REGEX = /^\\.+/;\n\nconst TOKEN_REGEX_CACHE = new Map<string, RegExp>();\nconst EXTENSION_REGEX_CACHE = new Map<string, RegExp>();\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(REGEX_SPECIAL_CHARACTERS_REGEX, '\\\\$&');\n}\n\nfunction getOptions(options: Options): ResolvedOptions {\n\treturn {\n\t\textensions: Array.isArray(options.extensions) ? options.extensions : [],\n\t\tattributes: Array.isArray(options.attributes) ? options.attributes : [],\n\t\tignoreFolders: Array.isArray(options.ignoreFolders) ? options.ignoreFolders : [],\n\t\tignoreFiles: Array.isArray(options.ignoreFiles) ? options.ignoreFiles : [],\n\t\tignoreDefaults: options.ignoreDefaults !== false,\n\t};\n}\n\n/**\n * Normalizes a user-supplied ignore token by trimming whitespace,\n * removing leading './' or '/', and stripping trailing '/' characters.\n *\n * @param path - The ignore token to normalize.\n *\n * @returns The normalized ignore token.\n */\nfunction cleanIgnoredPath(path: string): string {\n\treturn path.trim().replace(LEADING_RELATIVE_PREFIX_REGEX, '').replace(TRAILING_SLASHES_REGEX, '');\n}\n\n/**\n * Normalizes and de-duplicates ignore tokens by:\n * - Trimming whitespace from each path\n * - Removing leading './' or '/', and trailing '/' characters\n * - Dropping empty and root-only entries (such as `''`, `.`, `/`, and `./`)\n * - Removing duplicate entries in the result\n *\n * @param paths - An array of ignore path tokens to be cleaned and de-duplicated.\n *\n * @returns A new array containing unique, cleaned ignore tokens, with all empty or root-only entries omitted.\n */\nfunction cleanIgnoredPaths(paths: string[]): string[] {\n\tconst CLEANED = paths\n\t\t.filter((path): path is string => typeof path === 'string')\n\t\t.map(cleanIgnoredPath)\n\t\t.filter((path) => path !== '' && path !== '.');\n\n\treturn [...new Set(CLEANED)];\n}\n\n/**\n * Returns the combined and cleaned list of ignored paths based on the provided {@link ResolvedOptions}.\n *\n * @remarks\n * - Merges `ignoreFolders` and `ignoreFiles` into a single list.\n * - Cleans each entry using {@link cleanIgnoredPaths}.\n * - If `ignoreDefaults` is not explicitly set to `false`, the list also includes {@link DEFAULT_IGNORE_PATHS}.\n * - The resulting list is de-duplicated.\n *\n * @param options - The resolved options containing `ignoreFolders`, `ignoreFiles` and the `ignoreDefaults` flag.\n *\n * @returns An array of unique, cleaned ignore path tokens, including the built-in defaults unless\n * {@link ResolvedOptions.ignoreDefaults} is `false`.\n */\nfunction getIgnoredPaths(options: ResolvedOptions): string[] {\n\tconst CONFIGURED = cleanIgnoredPaths([...options.ignoreFolders, ...options.ignoreFiles]);\n\n\treturn options.ignoreDefaults ? [...new Set([...CONFIGURED, ...DEFAULT_IGNORE_PATHS])] : CONFIGURED;\n}\n\n/**\n * Removes the `?query` suffix that Vite appends to module IDs.\n *\n * For example, given `/src/App.svelte?svelte&type=style&lang.css`, this function will return `/src/App.svelte`.\n *\n * @param id - The module ID which may include a `?query` suffix.\n *\n * @returns The module ID without any `?query` part.\n */\nfunction stripQuery(id: string): string {\n\tconst QUERY_INDEX = id.indexOf('?');\n\n\treturn QUERY_INDEX === -1 ? id : id.slice(0, QUERY_INDEX);\n}\n\n/**\n * Returns the path of a module ID relative to the Vite root directory, using POSIX separators.\n *\n * @param id - The module ID to be converted to a relative path.\n * @param root - The root directory to which the path will be made relative.\n *\n * @returns The relative path from the root to the module ID, using '/' as the separator.\n */\nfunction toRelativePath(id: string, root: string): string {\n\treturn relative(root, stripQuery(id)).split(sep).join('/');\n}\n\n/**\n * Returns a cached or newly-created regular expression to match a path segment token,\n * supporting `*` wildcards matching within a segment (but not across path separators).\n *\n * The regular expression is built so that `*` matches any sequence of characters except '/'.\n * The regex is cached for subsequent calls with the same token.\n *\n * @param token - The ignore token, possibly containing `*` wildcards.\n *\n * @returns The regular expression corresponding to the token, matching segment boundaries.\n */\nfunction getTokenRegex(token: string): RegExp {\n\tconst CACHED = TOKEN_REGEX_CACHE.get(token);\n\n\tif (CACHED) {\n\t\treturn CACHED;\n\t}\n\n\t// `*` matches within a single segment; everything else is literal\n\tconst PATTERN = token.split('*').map(escapeRegExp).join('[^/]*');\n\t// eslint-disable-next-line security/detect-non-literal-regexp -- the token is regex-escaped above\n\tconst REGEX = new RegExp(`(?:^|/)${PATTERN}(?:/|$)`);\n\n\tTOKEN_REGEX_CACHE.set(token, REGEX);\n\n\treturn REGEX;\n}\n\n/**\n * Checks if the given relative path matches any of the provided ignore tokens,\n * comparing on path-segment boundaries. Ignores leading parent directory segments.\n *\n * @remarks\n * This function ignores absolute IDs to avoid false positives from folder names that coincidentally\n * contain ignore tokens. For example, in environments like Cloudflare where a repo may be cloned\n * to a directory such as `/opt/buildhome/repo`, a token like `build` should not match simply\n * because it's part of the parent path. Only the path relative to the Vite root is considered.\n *\n * Leading `../` segments are stripped from the path so that modules resolved outside the root\n * (e.g. `../../.pnpm/x/node_modules/y/index.js`) still have the opportunity to match ignore tokens\n * like `node_modules` against their segments.\n *\n * @param relativePath - The path of the module relative to the Vite root.\n * @param tokens - The list of ignore tokens, possibly including `*` wildcards.\n *\n * @returns `true` if the relative path matches any ignore token; otherwise, `false`.\n */\nfunction hasIgnorePath(relativePath: string, tokens: string[]): boolean {\n\tconst PATH = relativePath.replace(LEADING_PARENT_SEGMENTS_REGEX, '');\n\n\treturn tokens.some((token) => getTokenRegex(token).test(PATH));\n}\n\n/**\n * Returns a regular expression that matches any of the provided file extensions at the end of a string.\n *\n * The extensions are matched case-insensitively and can be provided with or without leading dots.\n * The resulting regex is cached for subsequent calls with the same set of extensions.\n *\n * @param extensions - An array of file extension strings (with or without leading dots).\n *\n * @returns A RegExp instance matching any of the specified extensions as a file suffix.\n */\nfunction getExtensionRegex(extensions: string[]): RegExp {\n\tconst KEY = extensions.join('|');\n\tconst CACHED = EXTENSION_REGEX_CACHE.get(KEY);\n\n\tif (CACHED) {\n\t\treturn CACHED;\n\t}\n\n\tconst PATTERN = extensions.map((extension) => escapeRegExp(extension.replace(LEADING_DOTS_REGEX, ''))).join('|');\n\t// eslint-disable-next-line security/detect-non-literal-regexp -- the extensions are regex-escaped above\n\tconst REGEX = new RegExp(`\\\\.(?:${PATTERN})$`, 'i');\n\n\tEXTENSION_REGEX_CACHE.set(KEY, REGEX);\n\n\treturn REGEX;\n}\n\n/**\n * Determines whether the provided module id (with query suffix stripped) ends with one of the specified extensions.\n *\n * @param id - The module identifier, possibly including a query suffix (e.g., `file.js?raw`).\n * @param extensions - An array of file extension strings (with or without leading dots).\n *\n * @returns `true` if the module id (excluding the query suffix) ends with one of the provided extensions;\n * otherwise, `false`.\n */\nfunction hasExtension(id: string, extensions: string[]): boolean {\n\tif (extensions.length === 0) {\n\t\treturn false;\n\t}\n\n\treturn getExtensionRegex(extensions).test(stripQuery(id));\n}\n\ninterface ExpressionFrame {\n\tkind: 'expression';\n\tdepth: number;\n}\n\ninterface TemplateFrame {\n\tkind: 'template';\n}\n\ntype Frame = ExpressionFrame | TemplateFrame;\n\ntype ScanStep = 'closed' | 'continue' | 'skip-next';\n\n/**\n * Character-by-character scanner for an `={…}` expression: tracks nested braces, quoted strings, template literals\n * and their `${…}` placeholders, and reports when the opening brace is closed\n */\nclass ExpressionScanner {\n\tprivate readonly frames: Frame[] = [{ kind: 'expression', depth: 1 }];\n\tprivate stringQuote = '';\n\tprivate isEscaped = false;\n\n\tstep(character: string, next: string): ScanStep {\n\t\tif (this.isEscaped) {\n\t\t\tthis.isEscaped = false;\n\n\t\t\treturn 'continue';\n\t\t}\n\n\t\tif (character === '\\\\') {\n\t\t\tthis.isEscaped = true;\n\n\t\t\treturn 'continue';\n\t\t}\n\n\t\tif (this.stringQuote !== '') {\n\t\t\tif (character === this.stringQuote) {\n\t\t\t\tthis.stringQuote = '';\n\t\t\t}\n\n\t\t\treturn 'continue';\n\t\t}\n\n\t\tconst FRAME = this.frames.at(-1);\n\n\t\tif (!FRAME) {\n\t\t\treturn 'continue';\n\t\t}\n\n\t\treturn FRAME.kind === 'template' ? this.stepTemplate(character, next) : this.stepExpression(FRAME, character);\n\t}\n\n\tprivate stepTemplate(character: string, next: string): ScanStep {\n\t\tif (character === '`') {\n\t\t\tthis.frames.pop();\n\t\t} else if (character === '$' && next === '{') {\n\t\t\tthis.frames.push({ kind: 'expression', depth: 1 });\n\n\t\t\treturn 'skip-next';\n\t\t}\n\n\t\treturn 'continue';\n\t}\n\n\tprivate stepExpression(frame: ExpressionFrame, character: string): ScanStep {\n\t\tif (character === \"'\" || character === '\"') {\n\t\t\tthis.stringQuote = character;\n\t\t} else if (character === '`') {\n\t\t\tthis.frames.push({ kind: 'template' });\n\t\t} else if (character === '{') {\n\t\t\tframe.depth++;\n\t\t} else if (character === '}') {\n\t\t\treturn this.closeBrace(frame);\n\t\t}\n\n\t\treturn 'continue';\n\t}\n\n\tprivate closeBrace(frame: ExpressionFrame): ScanStep {\n\t\tframe.depth--;\n\n\t\tif (frame.depth > 0) {\n\t\t\treturn 'continue';\n\t\t}\n\n\t\tif (this.frames.length === 1) {\n\t\t\treturn 'closed';\n\t\t}\n\n\t\tthis.frames.pop();\n\n\t\treturn 'continue';\n\t}\n}\n\n/**\n * Finds the closing brace for an `={…}` attribute value in a string of markup. Returns the index of the closing\n * brace, or `-1` if the braces are unbalanced or not found.\n *\n * @remarks\n * A regular expression cannot do this: the expression may hold a template literal whose `${…}` placeholders nest\n * further braces, and a brace inside a string is text rather than structure. The scanner tracks both.\n *\n * @param input - The input string containing the markup or code.\n * @param openingBraceIndex - The index of the opening `{` character to start scanning from.\n *\n * @returns The index of the corresponding closing `}` brace, or `-1` if unmatched.\n *\n * @example\n * ```ts\n * findExpressionEnd('foo={a + {b: `1${two}`}}', 4); // returns the index of the matching }\n * ```\n */\nfunction findExpressionEnd(input: string, openingBraceIndex: number): number {\n\tconst SCANNER = new ExpressionScanner();\n\n\tfor (let index = openingBraceIndex + 1; index < input.length; index++) {\n\t\tconst STEP = SCANNER.step(input.charAt(index), input.charAt(index + 1));\n\n\t\tif (STEP === 'closed') {\n\t\t\treturn index;\n\t\t}\n\n\t\tif (STEP === 'skip-next') {\n\t\t\tindex++;\n\t\t}\n\t}\n\n\treturn -1;\n}\n\n/**\n * Half-open `[start, end)` range of characters in the original input\n */\ntype Range = readonly [start: number, end: number];\n\n/**\n * Finds all ranges (start and end indices) for a specific attribute in the given markup input.\n *\n * Handles quoted values, expression values (i.e., `={...}`), and bare attributes, including optional\n * `:` or `v-bind:` prefixes. The detection accounts for whitespace before the attribute, as well as attributes\n * written on their own line. If an attribute value is an expression (`={...}`), this function uses\n * {@link findExpressionEnd} to locate the closing brace, and gracefully skips unbalanced or invalid expressions.\n *\n * @remarks\n * The returned ranges are suitable for text replacements: all whitespace before the attribute is included\n * so that removing the attribute does not leave a dangling blank line or extra spaces.\n *\n * @param input - The input markup string to search for the attribute within.\n * @param attribute - The name of the attribute to locate (e.g., `\"data-testid\"`).\n *\n * @returns An array of `[start, end)` ranges (as type `Range`) for each found occurrence of the attribute.\n */\nfunction findAttributeRangesFor(input: string, attribute: string): Range[] {\n\tconst NAME = escapeRegExp(attribute);\n\t// The leading `\\s+` swallows all whitespace before the attribute, so an attribute on its own line leaves no\n\t// blank line behind. Three forms follow: quoted value (group 1 = quote), expression value (group 2 = opening\n\t// brace, measured by `findExpressionEnd`), or bare attribute followed by whitespace, `/` or `>`\n\t// eslint-disable-next-line security/detect-non-literal-regexp -- the attribute name is regex-escaped above\n\tconst PATTERN = new RegExp(\n\t\t`\\\\s+(?::|v-bind:)?${NAME}(?:\\\\s*=\\\\s*(?:(['\"\\`])(?:(?!\\\\1)[\\\\s\\\\S])*\\\\1|(\\\\{))|(?=[\\\\s/>]))`,\n\t\t'gi',\n\t);\n\tconst RANGES: Range[] = [];\n\n\tlet match = PATTERN.exec(input);\n\n\twhile (match !== null) {\n\t\tconst MATCH_END = match.index + match[0].length;\n\n\t\tlet attributeEnd = MATCH_END;\n\n\t\tif (match[2] !== undefined) {\n\t\t\tconst CLOSING_BRACE_INDEX = findExpressionEnd(input, MATCH_END - 1);\n\n\t\t\t// An unbalanced expression means the source does not parse as written: leave it alone rather than\n\t\t\t// cutting the file at an arbitrary point\n\t\t\tif (CLOSING_BRACE_INDEX === -1) {\n\t\t\t\tmatch = PATTERN.exec(input);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattributeEnd = CLOSING_BRACE_INDEX + 1;\n\t\t}\n\n\t\tRANGES.push([match.index, attributeEnd]);\n\t\tPATTERN.lastIndex = attributeEnd;\n\t\tmatch = PATTERN.exec(input);\n\t}\n\n\treturn RANGES;\n}\n\n/**\n * Returns the merged ranges of every occurrence of the given attributes (in quoted, `={expression}`,\n * or bare form, with optional `:` or `v-bind:` prefix) within the markup string. The resulting\n * ranges are sorted by start index, and any touching or overlapping ranges are merged.\n *\n * All attributes are located in the same original input, so the returned ranges can be used\n * for sourcemap-aware or batch editing operations.\n *\n * @param input - The markup string to search for the attributes.\n * @param attributes - An array of attribute names to search for (e.g., `['data-testid', 'data-id']`).\n *\n * @returns An array of `Range` tuples, each representing [start, end) of a matched attribute occurrence,\n * with adjacent and overlapping ranges merged.\n */\nfunction findAttributeRanges(input: string, attributes: string[]): Range[] {\n\tconst RANGES = attributes\n\t\t.flatMap((attribute) => findAttributeRangesFor(input, attribute))\n\t\t.sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n\tconst MERGED: [number, number][] = [];\n\n\tfor (const [START, END] of RANGES) {\n\t\tconst LAST = MERGED.at(-1);\n\n\t\tif (LAST && START <= LAST[1]) {\n\t\t\tLAST[1] = Math.max(LAST[1], END);\n\t\t} else {\n\t\t\tMERGED.push([START, END]);\n\t\t}\n\t}\n\n\treturn MERGED;\n}\n\n/**\n * Returns a copy of the input string with the specified ranges removed.\n *\n * The provided ranges must be sorted by start index and non-overlapping.\n * Each range is specified as a tuple [start, end), with `start` inclusive and `end` exclusive.\n * The function concatenates the segments of the input string outside of these ranges.\n *\n * @param input - The original string from which to remove segments.\n * @param ranges - An array of `[start, end)` index ranges (sorted, non-overlapping) to cut out from the input.\n *\n * @returns The resulting string after all specified ranges have been removed from the input.\n */\nfunction removeRanges(input: string, ranges: Range[]): string {\n\tif (ranges.length === 0) {\n\t\treturn input;\n\t}\n\n\tconst SEGMENTS: string[] = [];\n\n\tlet lastEnd = 0;\n\n\tfor (const [START, END] of ranges) {\n\t\tSEGMENTS.push(input.slice(lastEnd, START));\n\t\tlastEnd = END;\n\t}\n\n\tSEGMENTS.push(input.slice(lastEnd));\n\n\treturn SEGMENTS.join('');\n}\n\n/**\n * Removes all occurrences of the specified attributes from the provided markup string.\n *\n * This function finds every instance of the given attribute names within the input markup (using\n * {@link findAttributeRanges}), then removes them, returning the markup with those attributes omitted.\n *\n * @param input - The original markup string from which attributes should be removed.\n * @param attributes - An array of attribute names to remove from the markup.\n *\n * @returns A new string representing the markup with the specified attributes removed.\n *\n * @see findAttributeRanges\n */\nfunction removeAttributes(input: string, attributes: string[]): string {\n\treturn removeRanges(input, findAttributeRanges(input, attributes));\n}\n\nexport type { Range };\nexport {\n\tcleanIgnoredPath,\n\tcleanIgnoredPaths,\n\tDEFAULT_IGNORE_PATHS,\n\tescapeRegExp,\n\tfindAttributeRanges,\n\tfindExpressionEnd,\n\tgetIgnoredPaths,\n\tgetOptions,\n\thasExtension,\n\thasIgnorePath,\n\tremoveAttributes,\n\tremoveRanges,\n\tstripQuery,\n\ttoRelativePath,\n};\n","import type { Plugin } from 'vite';\nimport type { Options } from './types';\n\nimport { generateRemovalSourceMap } from './sourcemap';\nimport {\n\tfindAttributeRanges,\n\tgetIgnoredPaths,\n\tgetOptions,\n\thasExtension,\n\thasIgnorePath,\n\tremoveRanges,\n\tstripQuery,\n\ttoRelativePath,\n} from './utilities';\n\nexport type { Options } from './types';\n\n/**\n * Vite plugin to remove specified attributes from markup files.\n *\n * This plugin scans files with configured extensions and removes\n * attributes as defined in the option list. It also produces\n * accurate source maps to ensure the original line and column numbers\n * are preserved for consumers of the Vite build chain.\n *\n * Files can be ignored based on path token matching. Ignore patterns\n * are resolved relative to the Vite root.\n *\n * @param options - Plugin configuration including which attributes\n * and file extensions to target, and optional ignore paths.\n *\n * @returns A Vite plugin object that removes attributes during\n * transformation steps.\n */\nfunction removeAttributesPlugin(options: Options): Plugin {\n\tconst OPTIONS = getOptions(options);\n\tconst IGNORED_PATHS = getIgnoredPaths(OPTIONS);\n\n\t// Ignore tokens are matched against paths relative to the Vite root, not to the process working directory\n\tlet root = process.cwd();\n\n\treturn {\n\t\tname: 'remove-attributes',\n\t\tenforce: 'pre',\n\t\tconfigResolved(config) {\n\t\t\troot = config.root;\n\t\t},\n\t\ttransform(code, id) {\n\t\t\t// Virtual modules (`\\0…`) never carry markup\n\t\t\tif (\n\t\t\t\tid.startsWith('\\0') ||\n\t\t\t\t!hasExtension(id, OPTIONS.extensions) ||\n\t\t\t\thasIgnorePath(toRelativePath(id, root), IGNORED_PATHS)\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst RANGES = findAttributeRanges(code, OPTIONS.attributes);\n\n\t\t\tif (RANGES.length === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// A removal shifts every later column (and every later line when the attribute sat on its own line), so the\n\t\t\t// map keeps the consumer's sourcemaps pointing at the original positions\n\t\t\treturn { code: removeRanges(code, RANGES), map: generateRemovalSourceMap(code, RANGES, stripQuery(id)) };\n\t\t},\n\t};\n}\n\nexport default removeAttributesPlugin;\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { Options as PluginOptions } from './types.cjs';
3
+ declare function removeAttributesPlugin(options: PluginOptions): Plugin;
4
+ declare namespace removeAttributesPlugin {
5
+ export type Options = PluginOptions;
6
+ }
7
+ export = removeAttributesPlugin;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,22 @@
1
- import type { Plugin } from 'vite';
2
- import type { Options } from './types';
3
- export default function removeAttributesPlugin(options: Options): Plugin;
4
- //# sourceMappingURL=index.d.ts.map
1
+ import { Plugin } from 'vite';
2
+ import { Options } from './types.js';
3
+ export type { Options } from './types.js';
4
+ /**
5
+ * Vite plugin to remove specified attributes from markup files.
6
+ *
7
+ * This plugin scans files with configured extensions and removes
8
+ * attributes as defined in the option list. It also produces
9
+ * accurate source maps to ensure the original line and column numbers
10
+ * are preserved for consumers of the Vite build chain.
11
+ *
12
+ * Files can be ignored based on path token matching. Ignore patterns
13
+ * are resolved relative to the Vite root.
14
+ *
15
+ * @param options - Plugin configuration including which attributes
16
+ * and file extensions to target, and optional ignore paths.
17
+ *
18
+ * @returns A Vite plugin object that removes attributes during
19
+ * transformation steps.
20
+ */
21
+ declare function removeAttributesPlugin(options: Options): Plugin;
22
+ export default removeAttributesPlugin;