prettier-plugin-bootstrap 0.1.2 → 0.2.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/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  [![CI](https://github.com/pierluigilenoci/prettier-plugin-bootstrap/actions/workflows/ci.yml/badge.svg)](https://github.com/pierluigilenoci/prettier-plugin-bootstrap/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/prettier-plugin-bootstrap)](https://www.npmjs.com/package/prettier-plugin-bootstrap)
5
+ [![codecov](https://codecov.io/gh/pierluigilenoci/prettier-plugin-bootstrap/branch/main/graph/badge.svg)](https://codecov.io/gh/pierluigilenoci/prettier-plugin-bootstrap)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
8
  A [Prettier](https://prettier.io/) plugin that automatically sorts Bootstrap CSS classes following the framework's recommended order.
8
9
 
9
- Works with **HTML**, **JSX/TSX**, **Vue**, **Angular**, and **Astro** templates.
10
+ Works with **HTML**, **JSX/TSX**, **Vue**, **Angular**, **Svelte**, and **Astro** templates.
10
11
 
11
12
  ## Installation
12
13
 
@@ -62,17 +63,18 @@ Unknown classes are preserved in their original relative order and placed after
62
63
 
63
64
  ## Options
64
65
 
65
- | Option | Type | Default | Description |
66
- |--------|------|---------|-------------|
67
- | `bootstrapAttributes` | `string[]` | `[]` | Additional HTML attributes to sort (beyond `class` and `className`) |
68
- | `bootstrapFunctions` | `string[]` | `[]` | Function names whose arguments are class lists (e.g. `clsx`, `classNames`) |
66
+ | Option | Type | Default | Description |
67
+ | --------------------- | ---------- | ------- | -------------------------------------------------------------------------- |
68
+ | `bootstrapAttributes` | `string[]` | `[]` | Additional HTML attributes to sort (beyond `class` and `className`) |
69
+ | `bootstrapFunctions` | `string[]` | `[]` | Function names whose arguments are class lists (e.g. `clsx`, `classNames`) |
69
70
 
70
71
  ### Example
71
72
 
72
73
  ```json
73
74
  {
74
75
  "plugins": ["prettier-plugin-bootstrap"],
75
- "bootstrapAttributes": ["ngClass", "v-bind:class"]
76
+ "bootstrapAttributes": ["ngClass", "v-bind:class"],
77
+ "bootstrapFunctions": ["clsx", "cn", "classNames"]
76
78
  }
77
79
  ```
78
80
 
@@ -83,8 +85,19 @@ Unknown classes are preserved in their original relative order and placed after
83
85
  - `angular` — Angular templates
84
86
  - `babel` / `babel-ts` / `typescript` — JSX/TSX files
85
87
  - `acorn` / `meriyah` — Alternative JS parsers
88
+ - `svelte` — Svelte components (requires `prettier-plugin-svelte`)
86
89
  - `astro` — Astro components (requires `prettier-plugin-astro`)
87
90
 
91
+ ### Svelte
92
+
93
+ When using with `prettier-plugin-svelte`, list `prettier-plugin-bootstrap` **after** `prettier-plugin-svelte` in your config:
94
+
95
+ ```json
96
+ {
97
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-bootstrap"]
98
+ }
99
+ ```
100
+
88
101
  ### Astro
89
102
 
90
103
  When using with `prettier-plugin-astro`, list `prettier-plugin-bootstrap` **after** `prettier-plugin-astro` in your config so it can wrap the Astro parser:
@@ -0,0 +1,39 @@
1
+ import { Options, Parser } from "prettier";
2
+
3
+ //#region src/types.d.ts
4
+ interface BootstrapPluginOptions extends Options {
5
+ bootstrapAttributes?: string[];
6
+ bootstrapFunctions?: string[];
7
+ }
8
+ type SortKey = [categoryIndex: number, breakpointIndex: number];
9
+ //#endregion
10
+ //#region src/class-order.d.ts
11
+ declare const BREAKPOINTS: readonly ["sm", "md", "lg", "xl", "xxl"];
12
+ declare const CLASS_ORDER: readonly string[];
13
+ declare function classKey(className: string): SortKey;
14
+ declare function sortClasses(classes: string[]): string[];
15
+ //#endregion
16
+ //#region src/index.d.ts
17
+ declare const options: {
18
+ bootstrapAttributes: {
19
+ type: "string";
20
+ array: boolean;
21
+ default: {
22
+ value: never[];
23
+ }[];
24
+ category: string;
25
+ description: string;
26
+ };
27
+ bootstrapFunctions: {
28
+ type: "string";
29
+ array: boolean;
30
+ default: {
31
+ value: never[];
32
+ }[];
33
+ category: string;
34
+ description: string;
35
+ };
36
+ };
37
+ declare const parsers: Record<string, Partial<Parser>>;
38
+ //#endregion
39
+ export { BREAKPOINTS, type BootstrapPluginOptions, CLASS_ORDER, classKey, options, parsers, sortClasses };
@@ -319,7 +319,35 @@ function walk(node, visitor) {
319
319
  }
320
320
  }
321
321
  }
322
- function processHtmlAst(ast, targetAttrs) {
322
+ function sortStringNode(node) {
323
+ if (!node) return;
324
+ if (node.type === "StringLiteral") {
325
+ const sorted = sortClassString(node.value);
326
+ node.value = sorted;
327
+ if (node.extra) {
328
+ node.extra.rawValue = sorted;
329
+ node.extra.raw = `"${sorted}"`;
330
+ }
331
+ return;
332
+ }
333
+ if (node.type === "Literal" && typeof node.value === "string") {
334
+ const sorted = sortClassString(node.value);
335
+ node.value = sorted;
336
+ if (node.raw) {
337
+ const quote = node.raw[0];
338
+ node.raw = `${quote}${sorted}${quote}`;
339
+ }
340
+ return;
341
+ }
342
+ if (node.type === "TemplateLiteral" && (!node.expressions || node.expressions.length === 0)) {
343
+ for (const quasi of node.quasis) if (quasi.value && typeof quasi.value.raw === "string") {
344
+ const sorted = sortClassString(quasi.value.raw);
345
+ quasi.value.raw = sorted;
346
+ quasi.value.cooked = sorted;
347
+ }
348
+ }
349
+ }
350
+ function processHtmlAst(ast, targetAttrs, _targetFunctions) {
323
351
  walk(ast, (node) => {
324
352
  if (node.attrs && Array.isArray(node.attrs)) {
325
353
  for (const attr of node.attrs) if (targetAttrs.includes(attr.name) && typeof attr.value === "string") attr.value = sortClassString(attr.value);
@@ -327,14 +355,21 @@ function processHtmlAst(ast, targetAttrs) {
327
355
  if (node.attributes && Array.isArray(node.attributes)) for (const attr of node.attributes) {
328
356
  const name = attr.name || attr.key && attr.key.value;
329
357
  if (targetAttrs.includes(name) && attr.value) {
330
- if (typeof attr.value === "string") attr.value = sortClassString(attr.value);
331
- else if (attr.value && typeof attr.value.value === "string") attr.value.value = sortClassString(attr.value.value);
358
+ if (typeof attr.value === "string") {
359
+ if (attr.kind && attr.kind !== "quoted") continue;
360
+ if (attr.value.includes("${")) continue;
361
+ attr.value = sortClassString(attr.value);
362
+ } else if (attr.value && typeof attr.value.value === "string") {
363
+ if (attr.value.value.includes("${")) continue;
364
+ attr.value.value = sortClassString(attr.value.value);
365
+ }
332
366
  }
333
367
  }
334
368
  });
335
369
  return ast;
336
370
  }
337
- function processJsxAst(ast, targetAttrs) {
371
+ function processJsxAst(ast, targetAttrs, targetFunctions = []) {
372
+ const functionSet = new Set(targetFunctions);
338
373
  walk(ast, (node) => {
339
374
  if (node.type === "JSXAttribute" || node.type === "JSXSpreadAttribute") {
340
375
  const name = node.name && (node.name.name || node.name.value);
@@ -353,6 +388,33 @@ function processJsxAst(ast, targetAttrs) {
353
388
  }
354
389
  }
355
390
  }
391
+ if (node.type === "CallExpression" && functionSet.size > 0) {
392
+ const callee = node.callee;
393
+ if (callee && callee.type === "Identifier" && functionSet.has(callee.name)) for (const arg of node.arguments || []) sortStringNode(arg);
394
+ }
395
+ });
396
+ return ast;
397
+ }
398
+ function walkSvelte(node, visitor) {
399
+ if (!node) return;
400
+ visitor(node);
401
+ if (node.fragment && Array.isArray(node.fragment.nodes)) for (const child of node.fragment.nodes) walkSvelte(child, visitor);
402
+ if (Array.isArray(node.children)) for (const child of node.children) walkSvelte(child, visitor);
403
+ }
404
+ function processSvelteAst(ast, targetAttrs, _targetFunctions) {
405
+ walkSvelte(ast, (node) => {
406
+ if (!node.attributes || !Array.isArray(node.attributes)) return;
407
+ for (const attr of node.attributes) {
408
+ if (attr.type !== "Attribute") continue;
409
+ if (!targetAttrs.includes(attr.name)) continue;
410
+ if (Array.isArray(attr.value)) {
411
+ for (const item of attr.value) if (item.type === "Text" && typeof item.data === "string") {
412
+ const sorted = sortClassString(item.data);
413
+ item.data = sorted;
414
+ if (typeof item.raw === "string") item.raw = sorted;
415
+ }
416
+ }
417
+ }
356
418
  });
357
419
  return ast;
358
420
  }
@@ -401,7 +463,7 @@ function createParserWrapper(parserName, processAst, astFormat) {
401
463
  if (!originalParser) throw new Error(`prettier-plugin-bootstrap: could not find the "${parserName}" parser. Make sure Prettier and the relevant parser plugin are installed.`);
402
464
  if (originalParser.locStart) wrapper.locStart = originalParser.locStart;
403
465
  if (originalParser.locEnd) wrapper.locEnd = originalParser.locEnd;
404
- return processAst(await originalParser.parse(text, options), [...DEFAULT_ATTRIBUTES, ...options.bootstrapAttributes || []]);
466
+ return processAst(await originalParser.parse(text, options), [...DEFAULT_ATTRIBUTES, ...options.bootstrapAttributes || []], options.bootstrapFunctions || []);
405
467
  }
406
468
  };
407
469
  return wrapper;
@@ -415,7 +477,8 @@ const parsers = {
415
477
  typescript: createParserWrapper("typescript", processJsxAst, "estree"),
416
478
  acorn: createParserWrapper("acorn", processJsxAst, "estree"),
417
479
  meriyah: createParserWrapper("meriyah", processJsxAst, "estree"),
418
- astro: createParserWrapper("astro", processHtmlAst, "astro")
480
+ astro: createParserWrapper("astro", processHtmlAst, "astro"),
481
+ svelte: createParserWrapper("svelte", processSvelteAst, "svelte-ast")
419
482
  };
420
483
  //#endregion
421
484
  export { BREAKPOINTS, CLASS_ORDER, classKey, options, parsers, sortClasses };
@@ -1,2 +1,3 @@
1
- export declare function processHtmlAst(ast: any, targetAttrs: string[]): any;
2
- export declare function processJsxAst(ast: any, targetAttrs: string[]): any;
1
+ export declare function processHtmlAst(ast: any, targetAttrs: string[], _targetFunctions?: string[]): any;
2
+ export declare function processJsxAst(ast: any, targetAttrs: string[], targetFunctions?: string[]): any;
3
+ export declare function processSvelteAst(ast: any, targetAttrs: string[], _targetFunctions?: string[]): any;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "prettier-plugin-bootstrap",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "A Prettier plugin for automatic Bootstrap class sorting",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./dist/index.js",
9
+ "import": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.ts"
11
11
  }
12
12
  },
@@ -38,14 +38,21 @@
38
38
  "prettier": "^3.0.0"
39
39
  },
40
40
  "devDependencies": {
41
+ "@eslint/js": "^10.0.1",
42
+ "@vitest/coverage-v8": "^3.2.4",
43
+ "eslint": "^10.3.0",
41
44
  "prettier": "^3.5.0",
42
- "tsdown": "^0.12.0",
45
+ "tsdown": "^0.21.10",
43
46
  "typescript": "^5.8.0",
47
+ "typescript-eslint": "^8.59.1",
44
48
  "vitest": "^3.1.0"
45
49
  },
46
50
  "scripts": {
47
51
  "build": "tsdown && tsc -p tsconfig.build.json",
52
+ "format:check": "prettier --check .",
53
+ "lint": "eslint .",
48
54
  "test": "vitest run",
55
+ "test:coverage": "vitest run --coverage",
49
56
  "test:watch": "vitest",
50
57
  "typecheck": "tsc --noEmit"
51
58
  }