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