svelte-qa-ids 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,357 @@
1
+ # svelte-qa-ids
2
+
3
+ > Automatically inject stable `data-qa-id` attributes into Svelte components for testing and automation
4
+
5
+ ## Features
6
+
7
+ - **Automated Injection**: Scans Svelte files and programmatically inserts `data-qa-id` attributes
8
+ - **Stable IDs**: Generates consistent, human-readable IDs based on component structure
9
+ - **Idempotent**: Removes old IDs before adding new ones for clean updates
10
+ - **Svelte 5 Compatible**: Uses `svelte.preprocess` for proper Svelte 5 syntax parsing
11
+ - **Smart Exclusion**: Skips icon libraries (e.g., `lucide-svelte`) and handles parsing errors gracefully
12
+ - **CLI & API**: Use as a command-line tool or programmatically in your build process
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -D svelte-qa-ids
18
+ # or
19
+ bun add -D svelte-qa-ids
20
+ ```
21
+
22
+ ## CLI Usage
23
+
24
+ ```bash
25
+ # Process all Svelte files in a directory
26
+ svelte-qa-ids src/
27
+
28
+ # Dry run to see what would change
29
+ svelte-qa-ids src/ --dry-run
30
+
31
+ # Process a single file
32
+ svelte-qa-ids src/routes/+page.svelte
33
+
34
+ # Verbose output
35
+ svelte-qa-ids src/ --verbose
36
+
37
+ # Custom component prefix length (2-10 characters)
38
+ svelte-qa-ids src/ --component 4
39
+ svelte-qa-ids src/ -c 2
40
+ ```
41
+
42
+ ## Programmatic Usage
43
+
44
+ ### Effect-based API (Primary)
45
+
46
+ The library is built with Effect-ts for type-safe error handling and composability:
47
+
48
+ ```typescript
49
+ import { inject } from 'svelte-qa-ids';
50
+ import { Effect } from 'effect';
51
+
52
+ // Process a directory
53
+ const program = inject('src/', {
54
+ verbose: true,
55
+ prefixLength: 4,
56
+ });
57
+
58
+ // Run the Effect
59
+ const results = await Effect.runPromise(program);
60
+
61
+ // Handle errors with Effect
62
+ const safeProgram = inject('src/', {
63
+ prefixLength: 4,
64
+ }).pipe(
65
+ Effect.catchAll((error) =>
66
+ Effect.succeed([
67
+ {
68
+ file: 'src/',
69
+ success: false,
70
+ error: error.message,
71
+ },
72
+ ])
73
+ )
74
+ );
75
+
76
+ const results = await Effect.runPromise(safeProgram);
77
+ ```
78
+
79
+ ### Promise-based API (Convenience)
80
+
81
+ For simpler use cases, a Promise-based wrapper is provided:
82
+
83
+ ```typescript
84
+ import { injectPromise } from 'svelte-qa-ids';
85
+
86
+ // Process a directory
87
+ const results = await injectPromise('src/', {
88
+ verbose: true,
89
+ });
90
+
91
+ // Process a single file
92
+ await injectPromise('src/routes/+page.svelte');
93
+
94
+ // With custom options
95
+ await injectPromise('src/', {
96
+ dryRun: true,
97
+ prefixLength: 4,
98
+ abbreviations: {
99
+ 'custom-button': 'cb',
100
+ },
101
+ skipComponents: ['MyIcon', 'AnotherIcon'],
102
+ });
103
+ ```
104
+
105
+ ## data-qa-id Generation Rules
106
+
107
+ ### Component Prefix
108
+
109
+ The component prefix is generated from the file path and can be customized with the `prefixLength` option (default: 3, range: 2-10).
110
+
111
+ **For pages (`src/routes/...`):**
112
+ - Uses first `prefixLength` letters of the route directory
113
+ - Example with default (prefixLength=3): `src/routes/analytics/+page.svelte` → `ana`
114
+ - Example with prefixLength=2: `src/routes/analytics/+page.svelte` → `an`
115
+ - Example with prefixLength=4: `src/routes/analytics/+page.svelte` → `anal`
116
+ - Root page (`src/routes/+page.svelte`) → `rt`
117
+
118
+ **For components (`src/lib/...`):**
119
+ - Uses first `prefixLength` letters of each word in filename
120
+ - Example with default (prefixLength=3): `upgrade-banner.svelte` → `upg-ban`
121
+ - Example with prefixLength=2: `upgrade-banner.svelte` → `up-ba`
122
+ - Example with prefixLength=4: `question-counter.svelte` → `quest-count`
123
+
124
+ ### Tag Abbreviations
125
+
126
+ Common tags are abbreviated for conciseness:
127
+
128
+ | Tag | Abbreviation |
129
+ |-----|--------------|
130
+ | `div` | `d` |
131
+ | `button` | `btn` |
132
+ | `input` | `in` |
133
+ | `label` | `lbl` |
134
+ | `span` | `sp` |
135
+ | `section` | `sec` |
136
+ | `form` | `f` |
137
+
138
+ ### Examples
139
+
140
+ ```html
141
+ <!-- Before: src/routes/splash/+page.svelte -->
142
+ <div class="container">
143
+ <h1>Welcome</h1>
144
+ <button>Continue</button>
145
+ </div>
146
+
147
+ <!-- After (default prefixLength=3) -->
148
+ <div class="container" data-qa-id="spl-d">
149
+ <h1 data-qa-id="spl-d-h1">Welcome</h1>
150
+ <button data-qa-id="spl-d-btn">Continue</button>
151
+ </div>
152
+
153
+ <!-- After (prefixLength=2) -->
154
+ <div class="container" data-qa-id="sp-d">
155
+ <h1 data-qa-id="sp-d-h1">Welcome</h1>
156
+ <button data-qa-id="sp-d-btn">Continue</button>
157
+ </div>
158
+
159
+ <!-- After (prefixLength=4) -->
160
+ <div class="container" data-qa-id="spla-d">
161
+ <h1 data-qa-id="spla-d-h1">Welcome</h1>
162
+ <button data-qa-id="spla-d-btn">Continue</button>
163
+ </div>
164
+ ```
165
+
166
+ **Component example** (`src/lib/question-counter.svelte`):
167
+
168
+ ```html
169
+ <!-- Before -->
170
+ <div class="counter">
171
+ <button id="increment">+</button>
172
+ <span>0</span>
173
+ </div>
174
+
175
+ <!-- After (default prefixLength=3) -->
176
+ <div class="counter" data-qa-id="que-d">
177
+ <button id="increment" data-qa-id="que-d-btn">+</button>
178
+ <span data-qa-id="que-d-sp">0</span>
179
+ </div>
180
+
181
+ <!-- After (prefixLength=4) -->
182
+ <div class="counter" data-qa-id="quest-d">
183
+ <button id="increment" data-qa-id="quest-d-btn">+</button>
184
+ <span data-qa-id="quest-d-sp">0</span>
185
+ </div>
186
+ ```
187
+
188
+ ## Integration with Build Process
189
+
190
+ ### SvelteKit
191
+
192
+ Add to your `package.json`:
193
+
194
+ ```json
195
+ {
196
+ "scripts": {
197
+ "prebuild": "svelte-qa-ids src/"
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Vite
203
+
204
+ ```javascript
205
+ // vite.config.ts
206
+ import { inject } from 'svelte-qa-ids';
207
+
208
+ export default {
209
+ plugins: [
210
+ {
211
+ name: 'inject-qa-ids',
212
+ async buildStart() {
213
+ await inject('src/');
214
+ }
215
+ }
216
+ ]
217
+ };
218
+ ```
219
+
220
+ ## Configuration Examples
221
+
222
+ ### Custom Prefix Length
223
+
224
+ Control how many characters from the component name are used for the ID prefix:
225
+
226
+ ```typescript
227
+ import { inject } from 'svelte-qa-ids';
228
+
229
+ // Use 2-character prefixes (more concise)
230
+ await inject('src/', { prefixLength: 2 });
231
+ // question-counter.svelte → qu-btn
232
+
233
+ // Use 4-character prefixes (more descriptive)
234
+ await inject('src/', { prefixLength: 4 });
235
+ // question-counter.svelte → quest-btn
236
+
237
+ // Default is 3 characters
238
+ await inject('src/');
239
+ // question-counter.svelte → que-btn
240
+ ```
241
+
242
+ ### CLI Prefix Length
243
+
244
+ ```bash
245
+ # Short prefix (2 chars)
246
+ svelte-qa-ids src/ --component 2
247
+
248
+ # Long prefix (5 chars)
249
+ svelte-qa-ids src/ --component 5
250
+ ```
251
+
252
+ ### Custom Abbreviations
253
+
254
+ Override default tag abbreviations:
255
+
256
+ ```typescript
257
+ await inject('src/', {
258
+ abbreviations: {
259
+ 'custom-button': 'cb',
260
+ 'data-table': 'dt',
261
+ 'user-card': 'uc',
262
+ },
263
+ });
264
+ ```
265
+
266
+ ### Skip Specific Components
267
+
268
+ Exclude certain components from processing:
269
+
270
+ ```typescript
271
+ await inject('src/', {
272
+ skipComponents: ['Icon', 'MyIcon', 'ThirdPartyIcon'],
273
+ });
274
+ ```
275
+
276
+ ## API Reference
277
+
278
+ ### `inject(path, options?)`
279
+
280
+ Injects `data-qa-id` attributes into Svelte files. This is the primary Effect-based API.
281
+
282
+ **Parameters:**
283
+
284
+ - `path` (`string`) - Path to a file or directory
285
+ - `options` (`Options`, optional) - Configuration options
286
+
287
+ **Returns:** `Effect<Result[], Error>`
288
+
289
+ Use `Effect.runPromise()` to execute:
290
+
291
+ ```typescript
292
+ import { inject } from 'svelte-qa-ids';
293
+ import { Effect } from 'effect';
294
+
295
+ const results = await Effect.runPromise(inject('src/'));
296
+ ```
297
+
298
+ ### `injectPromise(path, options?)`
299
+
300
+ Promise-based convenience wrapper for `inject()`.
301
+
302
+ **Parameters:**
303
+
304
+ - `path` (`string`) - Path to a file or directory
305
+ - `options` (`Options`, optional) - Configuration options
306
+
307
+ **Returns:** `Promise<Result[]>`
308
+
309
+ **Options:**
310
+
311
+ | Option | Type | Default | Description |
312
+ |--------|------|---------|-------------|
313
+ | `abbreviations` | `Record<string, string>` | built-in | Custom tag abbreviations |
314
+ | `prefixLength` | `number` | `3` | Characters from component name for prefix (2-10) |
315
+ | `prefixGenerator` | `(filePath: string, prefixLength?: number) => string` | built-in | Custom prefix function |
316
+ | `skipComponents` | `Set<string> \| string[]` | `lucide-svelte` | Components to skip |
317
+ | `include` | `string[]` | `undefined` | Include files matching glob patterns |
318
+ | `exclude` | `string[]` | `[]` | Exclude files matching glob patterns |
319
+ | `dryRun` | `boolean` | `false` | Don't modify files |
320
+ | `verbose` | `boolean` | `false` | Detailed logging |
321
+ | `preserveOnError` | `boolean` | `true` | Keep original content on parse errors |
322
+
323
+ **Result:**
324
+
325
+ ```typescript
326
+ interface Result {
327
+ file: string;
328
+ success: boolean;
329
+ error?: string;
330
+ injectedCount?: number;
331
+ skipped?: boolean;
332
+ }
333
+ ```
334
+
335
+ ## Known Limitations
336
+
337
+ ### Svelte 5 `{#let}` Blocks
338
+
339
+ Files containing `{#let}` blocks nested within `{#each}` loops may not be parseable by the standalone Svelte parser. These files are automatically skipped with a warning, preserving their original content.
340
+
341
+ This is a limitation of the Svelte compiler in standalone mode and affects files like:
342
+
343
+ ```svelte
344
+ {#each items as item}
345
+ {#let value = compute(item)}
346
+ <!-- content -->
347
+ {/let}
348
+ {/each}
349
+ ```
350
+
351
+ ## License
352
+
353
+ MIT
354
+
355
+ ## Contributing
356
+
357
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
5
+ import { basename, dirname, join, relative } from "path";
6
+ import { Data, Effect } from "effect";
7
+ import { parse, preprocess } from "svelte/compiler";
8
+ var FileReadError = class extends Data.TaggedError("FileReadError") {
9
+ };
10
+ var FileWriteError = class extends Data.TaggedError("FileWriteError") {
11
+ };
12
+ var SvelteParseError = class extends Data.TaggedError("SvelteParseError") {
13
+ };
14
+ var FileSystemError = class extends Data.TaggedError("FileSystemError") {
15
+ };
16
+ var ValidationError = class extends Data.TaggedError("ValidationError") {
17
+ };
18
+ var DEFAULT_ABBREVIATIONS = {
19
+ div: "d",
20
+ span: "sp",
21
+ section: "sec",
22
+ button: "btn",
23
+ a: "a",
24
+ p: "p",
25
+ img: "img",
26
+ ul: "ul",
27
+ li: "li",
28
+ form: "f",
29
+ input: "in",
30
+ label: "lbl",
31
+ h1: "h1",
32
+ h2: "h2",
33
+ h3: "h3",
34
+ h4: "h4",
35
+ h5: "h5",
36
+ h6: "h6"
37
+ };
38
+ var ROUTE_PREFIX_REGEX = /^src\/routes\/?/;
39
+ var validatePrefixLength = (prefixLength) => prefixLength !== void 0 && (prefixLength < 2 || prefixLength > 10) ? Effect.fail(
40
+ new ValidationError({
41
+ field: "prefixLength",
42
+ value: prefixLength,
43
+ message: "prefixLength must be between 2 and 10 characters"
44
+ })
45
+ ) : Effect.void;
46
+ function getTagAbbreviation(tagName, abbreviations = DEFAULT_ABBREVIATIONS) {
47
+ return abbreviations[tagName] || tagName;
48
+ }
49
+ var ROOT_ROUTE_PREFIX = "rt";
50
+ function defaultPrefixGenerator(filePath, prefixLength = 3) {
51
+ const projectRoot = process.cwd();
52
+ const relativePath = relative(projectRoot, filePath);
53
+ if (relativePath.startsWith("src/routes")) {
54
+ const routeDir = dirname(relativePath).replace(ROUTE_PREFIX_REGEX, "");
55
+ if (routeDir === "") {
56
+ return ROOT_ROUTE_PREFIX;
57
+ }
58
+ const lastPart = routeDir.split("/").pop() || "";
59
+ return lastPart.substring(0, Math.min(prefixLength, lastPart.length));
60
+ }
61
+ if (relativePath.startsWith("src/lib/")) {
62
+ const fileName = basename(filePath, ".svelte");
63
+ return fileName.split("-").map(
64
+ (part) => part.substring(0, Math.min(prefixLength, part.length))
65
+ ).join("-");
66
+ }
67
+ const baseName = basename(filePath, ".svelte");
68
+ return baseName.substring(0, Math.min(prefixLength, baseName.length));
69
+ }
70
+ function enhanceAst(node, parent = null) {
71
+ if (typeof node !== "object" || node === null) {
72
+ return;
73
+ }
74
+ node.parent = parent;
75
+ const childrenProps = [
76
+ "children",
77
+ "consequent",
78
+ "alternate",
79
+ "blocks",
80
+ "fragment"
81
+ ];
82
+ const allElementChildren = [];
83
+ const allChildrenNodes = [];
84
+ for (const prop of childrenProps) {
85
+ if (node[prop]) {
86
+ const childrenArray = Array.isArray(node[prop]) ? node[prop] : [node[prop]];
87
+ for (const child of childrenArray) {
88
+ if (child && typeof child === "object" && "type" in child) {
89
+ allChildrenNodes.push(child);
90
+ if (child.type === "Element") {
91
+ allElementChildren.push(child);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ const childElementsGroupedByTagName = {};
98
+ for (const child of allElementChildren) {
99
+ if (!childElementsGroupedByTagName[child.name]) {
100
+ childElementsGroupedByTagName[child.name] = [];
101
+ }
102
+ childElementsGroupedByTagName[child.name].push(child);
103
+ }
104
+ for (const child of allChildrenNodes) {
105
+ if (child.type === "Element") {
106
+ const siblingsOfType = childElementsGroupedByTagName[child.name];
107
+ child.siblingIndex = siblingsOfType.indexOf(child);
108
+ child.siblingCount = siblingsOfType.length;
109
+ }
110
+ enhanceAst(child, node);
111
+ }
112
+ }
113
+ function buildPathSegments(node, abbreviations = DEFAULT_ABBREVIATIONS) {
114
+ const pathSegments = [];
115
+ let tempNode = node;
116
+ while (tempNode && tempNode.parent) {
117
+ if (tempNode.type === "Element" && tempNode.siblingIndex !== void 0) {
118
+ const tagName = getTagAbbreviation(tempNode.name, abbreviations);
119
+ let segment = tagName;
120
+ if (tempNode.siblingCount > 1) {
121
+ segment += `-${tempNode.siblingIndex}`;
122
+ }
123
+ pathSegments.unshift(segment);
124
+ }
125
+ tempNode = tempNode.parent;
126
+ }
127
+ return pathSegments;
128
+ }
129
+ function getLucideComponentNames(ast) {
130
+ const lucideComponents = /* @__PURE__ */ new Set();
131
+ if (!(ast && typeof ast === "object" && "instance" in ast && ast.instance && typeof ast.instance === "object" && "content" in ast.instance && ast.instance.content && typeof ast.instance.content === "object" && "body" in ast.instance.content && Array.isArray(ast.instance.content.body))) {
132
+ return lucideComponents;
133
+ }
134
+ const instanceBody = ast.instance.content.body;
135
+ for (const node of instanceBody) {
136
+ if (typeof node === "object" && node !== null && "type" in node && node.type === "ImportDeclaration" && "source" in node && typeof node.source === "object" && node.source !== null && "value" in node.source && node.source.value === "lucide-svelte" && "specifiers" in node && Array.isArray(node.specifiers)) {
137
+ for (const specifier of node.specifiers) {
138
+ if (typeof specifier === "object" && specifier !== null && "type" in specifier && specifier.type === "ImportSpecifier" && "imported" in specifier && specifier.imported && typeof specifier.imported === "object" && "name" in specifier.imported && typeof specifier.imported.name === "string") {
139
+ lucideComponents.add(specifier.imported.name);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return lucideComponents;
145
+ }
146
+ function collectModifications(node, componentPrefix, content, lucideComponentNames, abbreviations = DEFAULT_ABBREVIATIONS) {
147
+ const modifications = [];
148
+ const usedInsertionPoints = /* @__PURE__ */ new Set();
149
+ function traverse(currentNode) {
150
+ if (typeof currentNode !== "object" || currentNode === null) {
151
+ return;
152
+ }
153
+ if ((currentNode.type === "Element" || currentNode.type === "InlineComponent") && lucideComponentNames.has(currentNode.name)) {
154
+ return;
155
+ }
156
+ if (currentNode.type === "Element" && (currentNode.name === "svg" || currentNode.name === "path")) {
157
+ return;
158
+ }
159
+ if (currentNode.type === "Element") {
160
+ const pathSegments = buildPathSegments(currentNode, abbreviations);
161
+ const qaId = `${componentPrefix}${pathSegments.length > 0 ? "-" : ""}${pathSegments.join("-")}`;
162
+ let pos = currentNode.start;
163
+ let inQuotes = false;
164
+ let quoteChar = "";
165
+ let inBraces = 0;
166
+ while (pos < currentNode.end) {
167
+ const char = content[pos];
168
+ const prevChar = pos > 0 ? content[pos - 1] : "";
169
+ if (!inQuotes && (char === '"' || char === "'")) {
170
+ if (prevChar !== "\\") {
171
+ inQuotes = true;
172
+ quoteChar = char;
173
+ }
174
+ } else if (inQuotes && char === quoteChar && prevChar !== "\\") {
175
+ inQuotes = false;
176
+ quoteChar = "";
177
+ } else if (!inQuotes) {
178
+ if (char === "{") {
179
+ inBraces++;
180
+ } else if (char === "}") {
181
+ inBraces--;
182
+ } else if (char === ">" && inBraces === 0) {
183
+ break;
184
+ }
185
+ }
186
+ pos++;
187
+ }
188
+ let insertionPoint = pos;
189
+ if (pos > currentNode.start + 1 && content[pos - 1] === "/") {
190
+ insertionPoint = pos - 1;
191
+ }
192
+ if (insertionPoint !== -1 && !usedInsertionPoints.has(insertionPoint)) {
193
+ usedInsertionPoints.add(insertionPoint);
194
+ modifications.push({ qaId, insertionPoint });
195
+ }
196
+ }
197
+ const childrenProps = [
198
+ "children",
199
+ "consequent",
200
+ "alternate",
201
+ "blocks",
202
+ "fragment"
203
+ ];
204
+ for (const prop of childrenProps) {
205
+ if (currentNode[prop]) {
206
+ const childrenArray = Array.isArray(currentNode[prop]) ? currentNode[prop] : [currentNode[prop]];
207
+ for (const child of childrenArray) {
208
+ traverse(child);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ traverse(node);
214
+ return modifications;
215
+ }
216
+ var readFile = (filePath) => Effect.try({
217
+ try: () => readFileSync(filePath, "utf-8"),
218
+ catch: (cause) => new FileReadError({ filePath, cause })
219
+ });
220
+ var writeFile = (filePath, content) => Effect.try({
221
+ try: () => writeFileSync(filePath, content, "utf-8"),
222
+ catch: (cause) => new FileWriteError({ filePath, cause })
223
+ });
224
+ var getStats = (path) => Effect.try({
225
+ try: () => statSync(path),
226
+ catch: (cause) => new FileSystemError({ path, cause })
227
+ });
228
+ var readDirectory = (path) => Effect.try({
229
+ try: () => readdirSync(path),
230
+ catch: (cause) => new FileSystemError({ path, cause })
231
+ });
232
+ var processFile = (filePath, options = {}) => Effect.gen(function* () {
233
+ const {
234
+ abbreviations = DEFAULT_ABBREVIATIONS,
235
+ prefixLength,
236
+ prefixGenerator = defaultPrefixGenerator,
237
+ skipComponents,
238
+ preserveOnError = true,
239
+ dryRun = false,
240
+ verbose = false
241
+ } = options;
242
+ yield* validatePrefixLength(prefixLength);
243
+ if (verbose) {
244
+ yield* Effect.log(`Processing file: ${filePath}`);
245
+ }
246
+ const originalContent = yield* readFile(filePath);
247
+ const componentPrefix = prefixGenerator === defaultPrefixGenerator ? prefixGenerator(filePath, prefixLength) : prefixGenerator(filePath);
248
+ const processed = yield* Effect.tryPromise({
249
+ try: () => preprocess(
250
+ originalContent,
251
+ {
252
+ markup: ({ content, filename }) => {
253
+ const originalContentForFallback = content;
254
+ const cleanContent = content.replace(
255
+ /\s*data-qa-id="[^"]*"/g,
256
+ ""
257
+ );
258
+ let modifiedContent = cleanContent;
259
+ try {
260
+ const ast = parse(cleanContent, { filename });
261
+ const lucideComponentNames = getLucideComponentNames(ast);
262
+ if (skipComponents) {
263
+ const skipSet = skipComponents instanceof Set ? skipComponents : new Set(skipComponents);
264
+ for (const comp of skipSet) {
265
+ lucideComponentNames.add(comp);
266
+ }
267
+ }
268
+ enhanceAst(ast.html);
269
+ const modifications = collectModifications(
270
+ ast.html,
271
+ componentPrefix,
272
+ cleanContent,
273
+ lucideComponentNames,
274
+ abbreviations
275
+ );
276
+ modifications.sort(
277
+ (a, b) => b.insertionPoint - a.insertionPoint
278
+ );
279
+ modifiedContent = cleanContent;
280
+ for (const mod of modifications) {
281
+ modifiedContent = modifiedContent.substring(0, mod.insertionPoint) + ` data-qa-id="${mod.qaId}"` + modifiedContent.substring(mod.insertionPoint);
282
+ }
283
+ } catch (parseError) {
284
+ const isLetBlockError = parseError.code === "expected_block_type" || parseError.message?.includes(
285
+ "{#let"
286
+ ) || parseError.message?.includes(
287
+ "Expected 'if', 'each', 'await', 'key' or 'snippet'"
288
+ );
289
+ if (isLetBlockError) {
290
+ return {
291
+ code: preserveOnError ? originalContentForFallback : cleanContent
292
+ };
293
+ }
294
+ throw parseError;
295
+ }
296
+ return { code: modifiedContent };
297
+ }
298
+ },
299
+ { filename: filePath }
300
+ ),
301
+ catch: (cause) => new SvelteParseError({ filePath, cause })
302
+ });
303
+ if (!dryRun) {
304
+ yield* writeFile(filePath, processed.code);
305
+ }
306
+ const injectedCount = (processed.code.match(/data-qa-id="[^"]*"/g) || []).length;
307
+ if (verbose && injectedCount > 0) {
308
+ yield* Effect.log(` Injected ${injectedCount} data-qa-id attributes`);
309
+ }
310
+ yield* Effect.log(
311
+ `Successfully updated data-qa-id attributes in ${filePath}.`
312
+ );
313
+ return {
314
+ file: filePath,
315
+ success: true,
316
+ injectedCount
317
+ };
318
+ }).pipe(
319
+ Effect.catchTag(
320
+ "FileReadError",
321
+ (error) => Effect.succeed({
322
+ file: filePath,
323
+ success: false,
324
+ error: `Error reading file: ${error.filePath}`
325
+ })
326
+ ),
327
+ Effect.catchTag(
328
+ "FileWriteError",
329
+ (error) => Effect.succeed({
330
+ file: filePath,
331
+ success: false,
332
+ error: `Error writing file: ${error.filePath}`
333
+ })
334
+ ),
335
+ Effect.catchTag(
336
+ "SvelteParseError",
337
+ (error) => Effect.succeed({
338
+ file: filePath,
339
+ success: false,
340
+ error: `Parse error in ${error.filePath}: ${error.cause instanceof Error ? error.cause.message : String(error.cause)}`
341
+ })
342
+ ),
343
+ Effect.catchTag(
344
+ "ValidationError",
345
+ (error) => Effect.succeed({
346
+ file: filePath,
347
+ success: false,
348
+ error: error.message
349
+ })
350
+ )
351
+ );
352
+ var processPath = (path, options = {}) => Effect.gen(function* () {
353
+ const results = [];
354
+ const traverse = (currentPath) => Effect.gen(function* () {
355
+ const stats = yield* getStats(currentPath);
356
+ if (stats.isFile()) {
357
+ if (currentPath.endsWith(".svelte")) {
358
+ const result = yield* processFile(currentPath, options);
359
+ results.push(result);
360
+ }
361
+ } else if (stats.isDirectory()) {
362
+ const files = yield* readDirectory(currentPath);
363
+ for (const file of files) {
364
+ yield* traverse(join(currentPath, file));
365
+ }
366
+ }
367
+ });
368
+ yield* traverse(path);
369
+ return results;
370
+ });
371
+ var inject = (path, options = {}) => Effect.gen(function* () {
372
+ const stats = yield* getStats(path);
373
+ if (stats.isFile()) {
374
+ const result = yield* processFile(path, options);
375
+ return [result];
376
+ }
377
+ if (stats.isDirectory()) {
378
+ return yield* processPath(path, options);
379
+ }
380
+ throw new FileSystemError({
381
+ path,
382
+ cause: "Path does not exist"
383
+ });
384
+ }).pipe(
385
+ Effect.catchTag("FileSystemError", (error) => Effect.die(error.cause))
386
+ );
387
+ async function injectPromise(path, options = {}) {
388
+ return Effect.runPromise(inject(path, options));
389
+ }
390
+
391
+ export {
392
+ FileReadError,
393
+ FileWriteError,
394
+ SvelteParseError,
395
+ FileSystemError,
396
+ ValidationError,
397
+ getTagAbbreviation,
398
+ defaultPrefixGenerator,
399
+ processFile,
400
+ processPath,
401
+ inject,
402
+ injectPromise
403
+ };
404
+ //# sourceMappingURL=chunk-7MRS72CP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * svelte-qa-ids\n * Automatically inject stable data-qa-id attributes into Svelte components\n */\n\nimport { readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join, relative } from \"node:path\";\nimport { Data, Effect } from \"effect\";\nimport { parse, preprocess } from \"svelte/compiler\";\n\n// ============================================================================\n// Error Types\n// ============================================================================\n\nexport class FileReadError extends Data.TaggedError(\"FileReadError\")<{\n readonly filePath: string;\n readonly cause: unknown;\n}> {}\n\nexport class FileWriteError extends Data.TaggedError(\"FileWriteError\")<{\n readonly filePath: string;\n readonly cause: unknown;\n}> {}\n\nexport class SvelteParseError extends Data.TaggedError(\"SvelteParseError\")<{\n readonly filePath: string;\n readonly cause: unknown;\n}> {}\n\nexport class FileSystemError extends Data.TaggedError(\"FileSystemError\")<{\n readonly path: string;\n readonly cause: unknown;\n}> {}\n\nexport class ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly value: unknown;\n readonly message: string;\n}> {}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface Options {\n /**\n * Custom abbreviations for HTML tags\n * @default { common abbreviations }\n */\n abbreviations?: Record<string, string>;\n\n /**\n * Number of characters from component name to use for ID prefix\n * @default 3\n * @throws {ValidationError} When value is outside 2-10 range\n * @validation Must be between 2 and 10 characters\n */\n prefixLength?: number;\n\n /**\n * Custom prefix generator function\n * @default built-in prefix generator\n */\n prefixGenerator?: (filePath: string) => string;\n\n /**\n * Component names to skip (e.g., icon libraries)\n * @default lucide-svelte components\n */\n skipComponents?: Set<string> | string[];\n\n /**\n * Include files matching these patterns\n */\n include?: string[];\n\n /**\n * Exclude files matching these patterns\n * @default {string[]} []\n */\n exclude?: string[];\n\n /**\n * Whether to preserve original content for files with parsing errors\n * @default true\n */\n preserveOnError?: boolean;\n\n /**\n * Dry run - don't modify files\n * @default false\n */\n dryRun?: boolean;\n\n /**\n * Verbose logging\n * @default false\n */\n verbose?: boolean;\n}\n\nexport interface Result {\n file: string;\n success: boolean;\n error?: string;\n injectedCount?: number;\n skipped?: boolean;\n}\n\n// ============================================================================\n// AST Types\n// ============================================================================\n\ninterface SvelteAstNode {\n type: string;\n name?: string;\n start?: number;\n end?: number;\n parent?: SvelteAstNode | null;\n siblingIndex?: number;\n siblingCount?: number;\n children?: SvelteAstNode[];\n consequent?: SvelteAstNode | SvelteAstNode[];\n alternate?: SvelteAstNode | SvelteAstNode[];\n blocks?: SvelteAstNode[];\n fragment?: SvelteAstNode[];\n [key: string]: unknown;\n}\n\ninterface ElementToModify {\n qaId: string;\n insertionPoint: number;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_ABBREVIATIONS: Record<string, string> = {\n div: \"d\",\n span: \"sp\",\n section: \"sec\",\n button: \"btn\",\n a: \"a\",\n p: \"p\",\n img: \"img\",\n ul: \"ul\",\n li: \"li\",\n form: \"f\",\n input: \"in\",\n label: \"lbl\",\n h1: \"h1\",\n h2: \"h2\",\n h3: \"h3\",\n h4: \"h4\",\n h5: \"h5\",\n h6: \"h6\",\n};\n\nconst ROUTE_PREFIX_REGEX = /^src\\/routes\\/?/;\n\n// ============================================================================\n// Core Functions\n// ============================================================================\n\n/**\n * Validates prefixLength is within acceptable range (2-10).\n */\nconst validatePrefixLength = (\n prefixLength?: number\n): Effect.Effect<void, ValidationError> =>\n prefixLength !== undefined && (prefixLength < 2 || prefixLength > 10)\n ? Effect.fail(\n new ValidationError({\n field: \"prefixLength\",\n value: prefixLength,\n message: \"prefixLength must be between 2 and 10 characters\",\n })\n )\n : Effect.void;\n\n/**\n * Returns the abbreviation for a given HTML tag.\n */\nexport { getTagAbbreviation };\n\nfunction getTagAbbreviation(\n tagName: string,\n abbreviations: Record<string, string> = DEFAULT_ABBREVIATIONS\n): string {\n return abbreviations[tagName] || tagName;\n}\n\n/**\n * Default component prefix generator based on file path.\n */\nconst ROOT_ROUTE_PREFIX = \"rt\" as const;\n\nexport { defaultPrefixGenerator };\n\nfunction defaultPrefixGenerator(filePath: string, prefixLength = 3): string {\n const projectRoot = process.cwd();\n const relativePath = relative(projectRoot, filePath);\n\n if (relativePath.startsWith(\"src/routes\")) {\n const routeDir = dirname(relativePath).replace(ROUTE_PREFIX_REGEX, \"\");\n if (routeDir === \"\") {\n return ROOT_ROUTE_PREFIX;\n }\n const lastPart = routeDir.split(\"/\").pop() || \"\";\n return lastPart.substring(0, Math.min(prefixLength, lastPart.length));\n }\n if (relativePath.startsWith(\"src/lib/\")) {\n const fileName = basename(filePath, \".svelte\");\n return fileName\n .split(\"-\")\n .map((part: string) =>\n part.substring(0, Math.min(prefixLength, part.length))\n )\n .join(\"-\");\n }\n\n const baseName = basename(filePath, \".svelte\");\n return baseName.substring(0, Math.min(prefixLength, baseName.length));\n}\n\n/**\n * Walks the Svelte AST and enhances nodes with parent pointers and sibling info.\n */\nfunction enhanceAst(\n node: SvelteAstNode,\n parent: SvelteAstNode | null = null\n): void {\n if (typeof node !== \"object\" || node === null) {\n return;\n }\n node.parent = parent;\n\n const childrenProps = [\n \"children\",\n \"consequent\",\n \"alternate\",\n \"blocks\",\n \"fragment\",\n ];\n const allElementChildren: SvelteAstNode[] = [];\n const allChildrenNodes: SvelteAstNode[] = [];\n\n for (const prop of childrenProps) {\n if (node[prop]) {\n const childrenArray = Array.isArray(node[prop])\n ? node[prop]\n : [node[prop]];\n for (const child of childrenArray) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n allChildrenNodes.push(child);\n if (child.type === \"Element\") {\n allElementChildren.push(child);\n }\n }\n }\n }\n }\n\n const childElementsGroupedByTagName: { [tagName: string]: SvelteAstNode[] } =\n {};\n for (const child of allElementChildren) {\n if (!childElementsGroupedByTagName[child.name!]) {\n childElementsGroupedByTagName[child.name!] = [];\n }\n childElementsGroupedByTagName[child.name!].push(child);\n }\n\n for (const child of allChildrenNodes) {\n if (child.type === \"Element\") {\n const siblingsOfType = childElementsGroupedByTagName[child.name!];\n child.siblingIndex = siblingsOfType.indexOf(child);\n child.siblingCount = siblingsOfType.length;\n }\n enhanceAst(child, node);\n }\n}\n\n/**\n * Builds the path segments for a data-qa-id by traversing up the AST.\n */\nfunction buildPathSegments(\n node: SvelteAstNode,\n abbreviations: Record<string, string> = DEFAULT_ABBREVIATIONS\n): string[] {\n const pathSegments: string[] = [];\n let tempNode: SvelteAstNode | null = node;\n while (tempNode && tempNode.parent) {\n if (tempNode.type === \"Element\" && tempNode.siblingIndex !== undefined) {\n const tagName = getTagAbbreviation(tempNode.name!, abbreviations);\n let segment = tagName;\n if (tempNode.siblingCount! > 1) {\n segment += `-${tempNode.siblingIndex}`;\n }\n pathSegments.unshift(segment);\n }\n tempNode = tempNode.parent;\n }\n return pathSegments;\n}\n\n/**\n * Extracts names of components imported from 'lucide-svelte'.\n */\nfunction getLucideComponentNames(ast: unknown): Set<string> {\n const lucideComponents = new Set<string>();\n if (\n !(\n ast &&\n typeof ast === \"object\" &&\n \"instance\" in ast &&\n ast.instance &&\n typeof ast.instance === \"object\" &&\n \"content\" in ast.instance &&\n ast.instance.content &&\n typeof ast.instance.content === \"object\" &&\n \"body\" in ast.instance.content &&\n Array.isArray(ast.instance.content.body)\n )\n ) {\n return lucideComponents;\n }\n\n const instanceBody = (ast.instance as { content: { body: unknown[] } })\n .content.body;\n\n for (const node of instanceBody) {\n if (\n typeof node === \"object\" &&\n node !== null &&\n \"type\" in node &&\n node.type === \"ImportDeclaration\" &&\n \"source\" in node &&\n typeof node.source === \"object\" &&\n node.source !== null &&\n \"value\" in node.source &&\n node.source.value === \"lucide-svelte\" &&\n \"specifiers\" in node &&\n Array.isArray(node.specifiers)\n ) {\n for (const specifier of node.specifiers) {\n if (\n typeof specifier === \"object\" &&\n specifier !== null &&\n \"type\" in specifier &&\n specifier.type === \"ImportSpecifier\" &&\n \"imported\" in specifier &&\n specifier.imported &&\n typeof specifier.imported === \"object\" &&\n \"name\" in specifier.imported &&\n typeof specifier.imported.name === \"string\"\n ) {\n lucideComponents.add(specifier.imported.name);\n }\n }\n }\n }\n return lucideComponents;\n}\n\n/**\n * Collects modifications to be made to the Svelte file.\n */\nfunction collectModifications(\n node: SvelteAstNode,\n componentPrefix: string,\n content: string,\n lucideComponentNames: Set<string>,\n abbreviations: Record<string, string> = DEFAULT_ABBREVIATIONS\n): ElementToModify[] {\n const modifications: ElementToModify[] = [];\n const usedInsertionPoints = new Set<number>();\n\n function traverse(currentNode: SvelteAstNode): void {\n if (typeof currentNode !== \"object\" || currentNode === null) {\n return;\n }\n\n // Skip Lucide Svelte components (both Element and InlineComponent types)\n if (\n (currentNode.type === \"Element\" ||\n currentNode.type === \"InlineComponent\") &&\n lucideComponentNames.has(currentNode.name!)\n ) {\n return;\n }\n\n // Skip SVG and path elements (icon internals) - only for Element type\n if (\n currentNode.type === \"Element\" &&\n (currentNode.name === \"svg\" || currentNode.name === \"path\")\n ) {\n return;\n }\n\n if (currentNode.type === \"Element\") {\n const pathSegments = buildPathSegments(currentNode, abbreviations);\n const qaId = `${componentPrefix}${\n pathSegments.length > 0 ? \"-\" : \"\"\n }${pathSegments.join(\"-\")}`;\n\n // Determine insertion point for data-qa-id attribute\n // First, find the end of the opening tag (first unquoted '>')\n let pos = currentNode.start!;\n let inQuotes = false;\n let quoteChar = \"\";\n let inBraces = 0; // Track {} for event handlers like onclick={() =>}\n\n while (pos < currentNode.end!) {\n const char = content[pos];\n const prevChar = pos > 0 ? content[pos - 1] : \"\";\n\n if (!inQuotes && (char === '\"' || char === \"'\")) {\n if (prevChar !== \"\\\\\") {\n inQuotes = true;\n quoteChar = char;\n }\n } else if (inQuotes && char === quoteChar && prevChar !== \"\\\\\") {\n inQuotes = false;\n quoteChar = \"\";\n } else if (!inQuotes) {\n if (char === \"{\") {\n inBraces++;\n } else if (char === \"}\") {\n inBraces--;\n } else if (char === \">\" && inBraces === 0) {\n // Found the closing '>' of the opening tag\n break;\n }\n }\n pos++;\n }\n\n // Now check if this is a self-closing tag (looks for '/>' before the '>')\n let insertionPoint = pos; // Default: insert before the '>'\n if (pos > currentNode.start! + 1 && content[pos - 1] === \"/\") {\n // Self-closing tag: insert before the '/'\n insertionPoint = pos - 1;\n }\n\n if (insertionPoint !== -1 && !usedInsertionPoints.has(insertionPoint)) {\n usedInsertionPoints.add(insertionPoint);\n modifications.push({ qaId, insertionPoint });\n }\n }\n\n const childrenProps = [\n \"children\",\n \"consequent\",\n \"alternate\",\n \"blocks\",\n \"fragment\",\n ];\n for (const prop of childrenProps) {\n if (currentNode[prop]) {\n const childrenArray = Array.isArray(currentNode[prop])\n ? currentNode[prop]\n : [currentNode[prop]];\n for (const child of childrenArray) {\n traverse(child);\n }\n }\n }\n }\n\n traverse(node);\n return modifications;\n}\n\n// ============================================================================\n// Effect-based File Operations\n// ============================================================================\n\n/**\n * Reads a file's content as an Effect.\n */\nconst readFile = (filePath: string): Effect.Effect<string, FileReadError> =>\n Effect.try({\n try: () => readFileSync(filePath, \"utf-8\"),\n catch: (cause) => new FileReadError({ filePath, cause }),\n });\n\n/**\n * Writes content to a file as an Effect.\n */\nconst writeFile = (\n filePath: string,\n content: string\n): Effect.Effect<void, FileWriteError> =>\n Effect.try({\n try: () => writeFileSync(filePath, content, \"utf-8\"),\n catch: (cause) => new FileWriteError({ filePath, cause }),\n });\n\n/**\n * Gets file stats as an Effect.\n */\nconst getStats = (\n path: string\n): Effect.Effect<ReturnType<typeof statSync>, FileSystemError> =>\n Effect.try({\n try: () => statSync(path),\n catch: (cause) => new FileSystemError({ path, cause }),\n });\n\n/**\n * Reads directory contents as an Effect.\n */\nconst readDirectory = (\n path: string\n): Effect.Effect<string[], FileSystemError> =>\n Effect.try({\n try: () => readdirSync(path),\n catch: (cause) => new FileSystemError({ path, cause }),\n });\n\n// ============================================================================\n// Processing Functions\n// ============================================================================\n\n/**\n * Processes a single Svelte component file to inject `data-qa-id` attributes.\n */\nexport const processFile = (\n filePath: string,\n options: Options = {}\n): Effect.Effect<\n Result,\n FileReadError | FileWriteError | SvelteParseError | ValidationError\n> =>\n Effect.gen(function* () {\n const {\n abbreviations = DEFAULT_ABBREVIATIONS,\n prefixLength,\n prefixGenerator = defaultPrefixGenerator,\n skipComponents,\n preserveOnError = true,\n dryRun = false,\n verbose = false,\n } = options;\n\n // Validate prefixLength if provided\n yield* validatePrefixLength(prefixLength);\n\n if (verbose) {\n yield* Effect.log(`Processing file: ${filePath}`);\n }\n\n const originalContent = yield* readFile(filePath);\n\n // Use defaultPrefixGenerator with prefixLength if no custom generator provided\n const componentPrefix =\n prefixGenerator === defaultPrefixGenerator\n ? prefixGenerator(filePath, prefixLength)\n : prefixGenerator(filePath);\n\n const processed = yield* Effect.tryPromise({\n try: () =>\n preprocess(\n originalContent,\n {\n markup: ({ content, filename }) => {\n const originalContentForFallback = content;\n const cleanContent = content.replace(\n /\\s*data-qa-id=\"[^\"]*\"/g,\n \"\"\n );\n let modifiedContent = cleanContent;\n\n try {\n const ast = parse(cleanContent, { filename });\n const lucideComponentNames = getLucideComponentNames(ast);\n\n // Add custom skip components\n if (skipComponents) {\n const skipSet =\n skipComponents instanceof Set\n ? skipComponents\n : new Set(skipComponents);\n for (const comp of skipSet) {\n lucideComponentNames.add(comp);\n }\n }\n\n enhanceAst(ast.html);\n const modifications = collectModifications(\n ast.html,\n componentPrefix,\n cleanContent,\n lucideComponentNames,\n abbreviations\n );\n\n modifications.sort(\n (a, b) => b.insertionPoint - a.insertionPoint\n );\n\n modifiedContent = cleanContent;\n for (const mod of modifications) {\n modifiedContent =\n modifiedContent.substring(0, mod.insertionPoint) +\n ` data-qa-id=\"${mod.qaId}\"` +\n modifiedContent.substring(mod.insertionPoint);\n }\n\n // Note: Verbose logging handled after processing completes\n } catch (parseError: unknown) {\n const isLetBlockError =\n (parseError as { code?: string }).code ===\n \"expected_block_type\" ||\n (parseError as { message?: string }).message?.includes(\n \"{#let\"\n ) ||\n (parseError as { message?: string }).message?.includes(\n \"Expected 'if', 'each', 'await', 'key' or 'snippet'\"\n );\n\n if (isLetBlockError) {\n // Note: Verbose logging handled after processing completes\n return {\n code: preserveOnError\n ? originalContentForFallback\n : cleanContent,\n };\n }\n throw parseError;\n }\n return { code: modifiedContent };\n },\n },\n { filename: filePath }\n ),\n catch: (cause) => new SvelteParseError({ filePath, cause }),\n });\n\n if (!dryRun) {\n yield* writeFile(filePath, processed.code);\n }\n\n const injectedCount = (processed.code.match(/data-qa-id=\"[^\"]*\"/g) || [])\n .length;\n\n if (verbose && injectedCount > 0) {\n yield* Effect.log(` Injected ${injectedCount} data-qa-id attributes`);\n }\n\n yield* Effect.log(\n `Successfully updated data-qa-id attributes in ${filePath}.`\n );\n\n return {\n file: filePath,\n success: true,\n injectedCount,\n };\n }).pipe(\n Effect.catchTag(\"FileReadError\", (error) =>\n Effect.succeed({\n file: filePath,\n success: false,\n error: `Error reading file: ${error.filePath}`,\n })\n ),\n Effect.catchTag(\"FileWriteError\", (error) =>\n Effect.succeed({\n file: filePath,\n success: false,\n error: `Error writing file: ${error.filePath}`,\n })\n ),\n Effect.catchTag(\"SvelteParseError\", (error) =>\n Effect.succeed({\n file: filePath,\n success: false,\n error: `Parse error in ${error.filePath}: ${\n error.cause instanceof Error\n ? error.cause.message\n : String(error.cause)\n }`,\n })\n ),\n Effect.catchTag(\"ValidationError\", (error) =>\n Effect.succeed({\n file: filePath,\n success: false,\n error: error.message,\n })\n )\n );\n\n/**\n * Recursively finds and processes all Svelte files in a given path.\n */\nexport const processPath = (\n path: string,\n options: Options = {}\n): Effect.Effect<\n Result[],\n | FileSystemError\n | FileReadError\n | FileWriteError\n | SvelteParseError\n | ValidationError\n> =>\n Effect.gen(function* () {\n const results: Result[] = [];\n\n const traverse = (\n currentPath: string\n ): Effect.Effect<\n void,\n | FileSystemError\n | FileReadError\n | FileWriteError\n | SvelteParseError\n | ValidationError\n > =>\n Effect.gen(function* () {\n const stats = yield* getStats(currentPath);\n\n if (stats.isFile()) {\n if (currentPath.endsWith(\".svelte\")) {\n const result = yield* processFile(currentPath, options);\n results.push(result);\n }\n } else if (stats.isDirectory()) {\n const files = yield* readDirectory(currentPath);\n for (const file of files) {\n yield* traverse(join(currentPath, file));\n }\n }\n });\n\n yield* traverse(path);\n return results;\n });\n\n/**\n * Main API - inject data-qa-id attributes into Svelte files\n */\nexport const inject = (\n path: string,\n options: Options = {}\n): Effect.Effect<\n Result[],\n | FileSystemError\n | FileReadError\n | FileWriteError\n | SvelteParseError\n | ValidationError\n> =>\n Effect.gen(function* () {\n const stats = yield* getStats(path);\n\n if (stats.isFile()) {\n const result = yield* processFile(path, options);\n return [result];\n }\n if (stats.isDirectory()) {\n return yield* processPath(path, options);\n }\n\n throw new FileSystemError({\n path,\n cause: \"Path does not exist\",\n });\n }).pipe(\n Effect.catchTag(\"FileSystemError\", (error) => Effect.die(error.cause))\n );\n\n/**\n * Promise-based API for compatibility\n */\nexport async function injectPromise(\n path: string,\n options: Options = {}\n): Promise<Result[]> {\n return Effect.runPromise(inject(path, options));\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ import {
4
+ injectPromise
5
+ } from "./chunk-7MRS72CP.js";
6
+
7
+ // src/cli.ts
8
+ import { resolve } from "path";
9
+ function parseArgs(args) {
10
+ const options = {
11
+ dryRun: false,
12
+ verbose: false,
13
+ help: false,
14
+ version: false
15
+ };
16
+ let path = "";
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+ switch (arg) {
20
+ case "-h":
21
+ case "--help":
22
+ options.help = true;
23
+ break;
24
+ case "-v":
25
+ case "--version":
26
+ options.version = true;
27
+ break;
28
+ case "-n":
29
+ case "--dry-run":
30
+ options.dryRun = true;
31
+ break;
32
+ case "--verbose":
33
+ options.verbose = true;
34
+ break;
35
+ case "-c":
36
+ case "--component": {
37
+ const nextArg = args[i + 1];
38
+ if (nextArg && !nextArg.startsWith("-")) {
39
+ options.componentLength = Number.parseInt(nextArg, 10);
40
+ i++;
41
+ }
42
+ break;
43
+ }
44
+ default:
45
+ if (!arg.startsWith("-")) {
46
+ path = arg;
47
+ }
48
+ break;
49
+ }
50
+ }
51
+ return { path, options };
52
+ }
53
+ function showHelp() {
54
+ console.log(`
55
+ svelte-qa-ids - Automatically inject data-qa-id attributes into Svelte components
56
+
57
+ USAGE:
58
+ svelte-qa-ids <path> [options]
59
+
60
+ ARGUMENTS:
61
+ <path> Path to a Svelte file or directory to process
62
+
63
+ OPTIONS:
64
+ -h, --help Show this help message
65
+ -v, --version Show version number
66
+ -n, --dry-run Process files without modifying them
67
+ --verbose Show detailed logging
68
+ -c, --component <N> Number of characters for component prefix (2-10, default: 3)
69
+
70
+ EXAMPLES:
71
+ # Process all Svelte files in src/
72
+ svelte-qa-ids src/
73
+
74
+ # Dry run to see what would be changed
75
+ svelte-qa-ids src/ --dry-run
76
+
77
+ # Process a single file
78
+ svelte-qa-ids src/routes/+page.svelte
79
+
80
+ # Verbose output
81
+ svelte-qa-ids src/ --verbose
82
+
83
+ # Use 4-character component prefixes
84
+ svelte-qa-ids src/ --component 4
85
+
86
+ # Use 2-character component prefixes
87
+ svelte-qa-ids src/ -c 2
88
+
89
+ For more information, visit: https://github.com/your-org/svelte-qa-ids
90
+ `);
91
+ }
92
+ async function showVersion() {
93
+ const pkg = await import("./package-QZOBPTUC.js");
94
+ console.log(`svelte-qa-ids v${pkg.default.version}`);
95
+ }
96
+ async function main() {
97
+ const args = process.argv.slice(2);
98
+ const { path, options } = parseArgs(args);
99
+ if (options.help) {
100
+ showHelp();
101
+ process.exit(0);
102
+ }
103
+ if (options.version) {
104
+ await showVersion();
105
+ process.exit(0);
106
+ }
107
+ if (!path) {
108
+ console.error("Error: Please provide a path to process");
109
+ console.error("Run 'svelte-qa-ids --help' for usage information");
110
+ process.exit(1);
111
+ }
112
+ const resolvedPath = resolve(path);
113
+ const injectOptions = {
114
+ dryRun: options.dryRun,
115
+ verbose: options.verbose,
116
+ prefixLength: options.componentLength
117
+ };
118
+ if (options.dryRun) {
119
+ console.log("DRY RUN MODE - No files will be modified\n");
120
+ }
121
+ try {
122
+ const results = await injectPromise(resolvedPath, injectOptions);
123
+ const successCount = results.filter((r) => r.success).length;
124
+ const failCount = results.filter((r) => !r.success).length;
125
+ console.log(`
126
+ Processed ${results.length} file(s)`);
127
+ console.log(` Success: ${successCount}`);
128
+ console.log(` Failed: ${failCount}`);
129
+ if (failCount > 0) {
130
+ console.log("\nErrors:");
131
+ for (const result of results) {
132
+ if (!result.success) {
133
+ console.error(` ${result.file}: ${result.error}`);
134
+ }
135
+ }
136
+ process.exit(1);
137
+ }
138
+ } catch (error) {
139
+ console.error("Error:", error instanceof Error ? error.message : error);
140
+ process.exit(1);
141
+ }
142
+ }
143
+ main();
144
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI for svelte-qa-ids\n */\n\nimport { resolve } from \"node:path\";\nimport { injectPromise, type Options } from \"./index.js\";\n\ninterface CliOptions {\n dryRun: boolean;\n verbose: boolean;\n help: boolean;\n version: boolean;\n componentLength?: number;\n}\n\nfunction parseArgs(args: string[]): { path: string; options: CliOptions } {\n const options: CliOptions = {\n dryRun: false,\n verbose: false,\n help: false,\n version: false,\n };\n\n let path = \"\";\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n switch (arg) {\n case \"-h\":\n case \"--help\":\n options.help = true;\n break;\n case \"-v\":\n case \"--version\":\n options.version = true;\n break;\n case \"-n\":\n case \"--dry-run\":\n options.dryRun = true;\n break;\n case \"--verbose\":\n options.verbose = true;\n break;\n case \"-c\":\n case \"--component\": {\n const nextArg = args[i + 1];\n if (nextArg && !nextArg.startsWith(\"-\")) {\n options.componentLength = Number.parseInt(nextArg, 10);\n i++; // Skip next arg\n }\n break;\n }\n default:\n if (!arg.startsWith(\"-\")) {\n path = arg;\n }\n break;\n }\n }\n\n return { path, options };\n}\n\nfunction showHelp(): void {\n console.log(`\nsvelte-qa-ids - Automatically inject data-qa-id attributes into Svelte components\n\nUSAGE:\n svelte-qa-ids <path> [options]\n\nARGUMENTS:\n <path> Path to a Svelte file or directory to process\n\nOPTIONS:\n -h, --help Show this help message\n -v, --version Show version number\n -n, --dry-run Process files without modifying them\n --verbose Show detailed logging\n -c, --component <N> Number of characters for component prefix (2-10, default: 3)\n\nEXAMPLES:\n # Process all Svelte files in src/\n svelte-qa-ids src/\n\n # Dry run to see what would be changed\n svelte-qa-ids src/ --dry-run\n\n # Process a single file\n svelte-qa-ids src/routes/+page.svelte\n\n # Verbose output\n svelte-qa-ids src/ --verbose\n\n # Use 4-character component prefixes\n svelte-qa-ids src/ --component 4\n\n # Use 2-character component prefixes\n svelte-qa-ids src/ -c 2\n\nFor more information, visit: https://github.com/your-org/svelte-qa-ids\n`);\n}\n\nasync function showVersion(): Promise<void> {\n const pkg = await import(\"../package.json\", {\n assert: { type: \"json\" },\n });\n console.log(`svelte-qa-ids v${pkg.default.version}`);\n}\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n const { path, options } = parseArgs(args);\n\n if (options.help) {\n showHelp();\n process.exit(0);\n }\n\n if (options.version) {\n await showVersion();\n process.exit(0);\n }\n\n if (!path) {\n console.error(\"Error: Please provide a path to process\");\n console.error(\"Run 'svelte-qa-ids --help' for usage information\");\n process.exit(1);\n }\n\n const resolvedPath = resolve(path);\n const injectOptions: Options = {\n dryRun: options.dryRun,\n verbose: options.verbose,\n prefixLength: options.componentLength,\n };\n\n if (options.dryRun) {\n console.log(\"DRY RUN MODE - No files will be modified\\n\");\n }\n\n try {\n const results = await injectPromise(resolvedPath, injectOptions);\n\n const successCount = results.filter((r) => r.success).length;\n const failCount = results.filter((r) => !r.success).length;\n\n console.log(`\\nProcessed ${results.length} file(s)`);\n console.log(` Success: ${successCount}`);\n console.log(` Failed: ${failCount}`);\n\n if (failCount > 0) {\n console.log(\"\\nErrors:\");\n for (const result of results) {\n if (!result.success) {\n console.error(` ${result.file}: ${result.error}`);\n }\n }\n process.exit(1);\n }\n } catch (error) {\n console.error(\"Error:\", error instanceof Error ? error.message : error);\n process.exit(1);\n }\n}\n\nmain();\n"],"mappings":"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","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FileReadError,
4
+ FileSystemError,
5
+ FileWriteError,
6
+ SvelteParseError,
7
+ ValidationError,
8
+ defaultPrefixGenerator,
9
+ getTagAbbreviation,
10
+ inject,
11
+ injectPromise,
12
+ processFile,
13
+ processPath
14
+ } from "./chunk-7MRS72CP.js";
15
+ export {
16
+ FileReadError,
17
+ FileSystemError,
18
+ FileWriteError,
19
+ SvelteParseError,
20
+ ValidationError,
21
+ defaultPrefixGenerator,
22
+ getTagAbbreviation,
23
+ inject,
24
+ injectPromise,
25
+ processFile,
26
+ processPath
27
+ };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ // package.json
4
+ var name = "svelte-qa-ids";
5
+ var version = "1.0.0";
6
+ var description = "Automatically inject stable data-qa-id attributes into Svelte components for testing and automation";
7
+ var type = "module";
8
+ var main = "./dist/index.js";
9
+ var bin = {
10
+ "svelte-qa-ids": "dist/cli.js"
11
+ };
12
+ var exports = {
13
+ ".": {
14
+ import: "./dist/index.js",
15
+ types: "./dist/index.d.ts"
16
+ }
17
+ };
18
+ var files = [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ];
23
+ var scripts = {
24
+ build: "tsup",
25
+ dev: "tsup --watch",
26
+ test: "bun test",
27
+ lint: "bun x ultracite check src",
28
+ format: "bun x ultracite fix src",
29
+ prepublishOnly: "bun run build",
30
+ check: "ultracite check",
31
+ fix: "ultracite fix",
32
+ prepare: "lefthook install"
33
+ };
34
+ var keywords = [
35
+ "svelte",
36
+ "testing",
37
+ "automation",
38
+ "qa",
39
+ "data-qa-id",
40
+ "e2e",
41
+ "playwright",
42
+ "selectors"
43
+ ];
44
+ var author = "";
45
+ var license = "MIT";
46
+ var repository = {
47
+ type: "git",
48
+ url: "git+https://github.com/edut/svelte-qa-ids.git"
49
+ };
50
+ var peerDependencies = {
51
+ svelte: "^3.0.0 || ^4.0.0 || ^5.0.0"
52
+ };
53
+ var devDependencies = {
54
+ "@biomejs/biome": "2.3.13",
55
+ "@types/node": "^20.17.6",
56
+ lefthook: "^2.0.16",
57
+ tsup: "^8.3.5",
58
+ typescript: "^5.7.2",
59
+ ultracite: "7.1.3"
60
+ };
61
+ var dependencies = {
62
+ effect: "^3.19.15",
63
+ svelte: "^5.3.0"
64
+ };
65
+ var engines = {
66
+ node: ">=18.0.0"
67
+ };
68
+ var package_default = {
69
+ name,
70
+ version,
71
+ description,
72
+ type,
73
+ main,
74
+ bin,
75
+ exports,
76
+ files,
77
+ scripts,
78
+ keywords,
79
+ author,
80
+ license,
81
+ repository,
82
+ peerDependencies,
83
+ devDependencies,
84
+ dependencies,
85
+ engines
86
+ };
87
+ export {
88
+ author,
89
+ bin,
90
+ package_default as default,
91
+ dependencies,
92
+ description,
93
+ devDependencies,
94
+ engines,
95
+ exports,
96
+ files,
97
+ keywords,
98
+ license,
99
+ main,
100
+ name,
101
+ peerDependencies,
102
+ repository,
103
+ scripts,
104
+ type,
105
+ version
106
+ };
107
+ //# sourceMappingURL=package-QZOBPTUC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"svelte-qa-ids\",\n \"version\": \"1.0.0\",\n \"description\": \"Automatically inject stable data-qa-id attributes into Svelte components for testing and automation\",\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"bin\": {\n \"svelte-qa-ids\": \"dist/cli.js\"\n },\n \"exports\": {\n \".\": {\n \"import\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"bun test\",\n \"lint\": \"bun x ultracite check src\",\n \"format\": \"bun x ultracite fix src\",\n \"prepublishOnly\": \"bun run build\",\n \"check\": \"ultracite check\",\n \"fix\": \"ultracite fix\",\n \"prepare\": \"lefthook install\"\n },\n \"keywords\": [\n \"svelte\",\n \"testing\",\n \"automation\",\n \"qa\",\n \"data-qa-id\",\n \"e2e\",\n \"playwright\",\n \"selectors\"\n ],\n \"author\": \"\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/edut/svelte-qa-ids.git\"\n },\n \"peerDependencies\": {\n \"svelte\": \"^3.0.0 || ^4.0.0 || ^5.0.0\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.3.13\",\n \"@types/node\": \"^20.17.6\",\n \"lefthook\": \"^2.0.16\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.7.2\",\n \"ultracite\": \"7.1.3\"\n },\n \"dependencies\": {\n \"effect\": \"^3.19.15\",\n \"svelte\": \"^5.3.0\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n"],"mappings":";;;AACE,WAAQ;AACR,cAAW;AACX,kBAAe;AACf,WAAQ;AACR,WAAQ;AACR,UAAO;AAAA,EACL,iBAAiB;AACnB;AACA,cAAW;AAAA,EACT,KAAK;AAAA,IACH,QAAU;AAAA,IACV,OAAS;AAAA,EACX;AACF;AACA,YAAS;AAAA,EACP;AAAA,EACA;AAAA,EACA;AACF;AACA,cAAW;AAAA,EACT,OAAS;AAAA,EACT,KAAO;AAAA,EACP,MAAQ;AAAA,EACR,MAAQ;AAAA,EACR,QAAU;AAAA,EACV,gBAAkB;AAAA,EAClB,OAAS;AAAA,EACT,KAAO;AAAA,EACP,SAAW;AACb;AACA,eAAY;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,aAAU;AACV,cAAW;AACX,iBAAc;AAAA,EACZ,MAAQ;AAAA,EACR,KAAO;AACT;AACA,uBAAoB;AAAA,EAClB,QAAU;AACZ;AACA,sBAAmB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,UAAY;AAAA,EACZ,MAAQ;AAAA,EACR,YAAc;AAAA,EACd,WAAa;AACf;AACA,mBAAgB;AAAA,EACd,QAAU;AAAA,EACV,QAAU;AACZ;AACA,cAAW;AAAA,EACT,MAAQ;AACV;AAhEF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAGA;AAAA,EAMA;AAAA,EAKA;AAAA,EAWA;AAAA,EAUA;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,EAGA;AAAA,EAQA;AAAA,EAIA;AAGF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "svelte-qa-ids",
3
+ "version": "1.0.0",
4
+ "description": "Automatically inject stable data-qa-id attributes into Svelte components for testing and automation",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "svelte-qa-ids": "dist/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "test": "bun test",
25
+ "lint": "bun x ultracite check src",
26
+ "format": "bun x ultracite fix src",
27
+ "prepublishOnly": "bun run build",
28
+ "check": "ultracite check",
29
+ "fix": "ultracite fix",
30
+ "prepare": "lefthook install"
31
+ },
32
+ "keywords": [
33
+ "svelte",
34
+ "testing",
35
+ "automation",
36
+ "qa",
37
+ "data-qa-id",
38
+ "e2e",
39
+ "playwright",
40
+ "selectors"
41
+ ],
42
+ "author": "",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/edut/svelte-qa-ids.git"
47
+ },
48
+ "peerDependencies": {
49
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "2.3.13",
53
+ "@types/node": "^20.17.6",
54
+ "lefthook": "^2.0.16",
55
+ "tsup": "^8.3.5",
56
+ "typescript": "^5.7.2",
57
+ "ultracite": "7.1.3"
58
+ },
59
+ "dependencies": {
60
+ "effect": "^3.19.15",
61
+ "svelte": "^5.3.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ }
66
+ }