prettier-plugin-sort 0.0.1 → 0.0.3
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 +32 -3
- package/README.zh.md +30 -1
- package/dist/index.js +98 -72
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -146,8 +146,8 @@ The shape of `importOrderTypeImports` mirrors conventions in the ESLint ecosyste
|
|
|
146
146
|
|
|
147
147
|
Using `import { c, type B, a } from 'mod';` as an example:
|
|
148
148
|
|
|
149
|
-
| Mode | Result
|
|
150
|
-
| -------------- |
|
|
149
|
+
| Mode | Result |
|
|
150
|
+
| -------------- | ---------------------------------------------------------------- |
|
|
151
151
|
| `separate` | `import type { B } from 'mod';`<br>`import { a, c } from 'mod';` |
|
|
152
152
|
| `inline-first` | `import { type B, a, c } from 'mod';` |
|
|
153
153
|
| `inline-last` | `import { a, c, type B } from 'mod';` |
|
|
@@ -182,6 +182,35 @@ import { useEffect, useState } from 'react';
|
|
|
182
182
|
|
|
183
183
|
Set `importOrderMergeDuplicates` to `false` if you want to keep the original separate statements. Side-effect imports (`import 'mod';`) are never merged because their order has runtime semantics.
|
|
184
184
|
|
|
185
|
+
#### Side-effect imports
|
|
186
|
+
|
|
187
|
+
The order of side-effect imports (`import 'mod'`) often carries runtime meaning, such as CSS cascade order or polyfills that must load before a framework. The plugin never moves other imports across a side-effect import — imports on each side are sorted independently, and the side-effect import itself stays in place.
|
|
188
|
+
|
|
189
|
+
Before:
|
|
190
|
+
|
|
191
|
+
<!-- prettier-ignore -->
|
|
192
|
+
```typescript
|
|
193
|
+
import Button from './Button';
|
|
194
|
+
import App from './App';
|
|
195
|
+
import 'normalize.css';
|
|
196
|
+
import theme from './theme';
|
|
197
|
+
import Icon from './Icon';
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
After:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import App from './App';
|
|
204
|
+
import Button from './Button';
|
|
205
|
+
|
|
206
|
+
import 'normalize.css';
|
|
207
|
+
|
|
208
|
+
import Icon from './Icon';
|
|
209
|
+
import theme from './theme';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Imports on each side are sorted independently. The side-effect import itself stays in place.
|
|
213
|
+
|
|
185
214
|
Sorting rules:
|
|
186
215
|
|
|
187
216
|
- Imports are classified into groups. Within each group they are sorted alphabetically
|
|
@@ -189,7 +218,7 @@ Sorting rules:
|
|
|
189
218
|
- A blank line is inserted between groups by default. Disable with `importOrderSeparation`
|
|
190
219
|
- `type` imports are split into their own statement by default. Use `importOrderTypeImports` to inline them instead
|
|
191
220
|
- Multiple imports from the same source are merged into one by default. Disable with `importOrderMergeDuplicates`
|
|
192
|
-
- Side-effect imports (`import 'mod'`) are never
|
|
221
|
+
- Side-effect imports (`import 'mod'`) carry semantic order and are never moved. Imports on either side are sorted independently
|
|
193
222
|
|
|
194
223
|
### Exports
|
|
195
224
|
|
package/README.zh.md
CHANGED
|
@@ -185,6 +185,35 @@ import { useEffect, useState } from 'react';
|
|
|
185
185
|
|
|
186
186
|
如果你希望保留原本分离的两条语句,把 `importOrderMergeDuplicates` 设为 `false` 即可。副作用导入(`import 'mod';`)因为顺序有语义,永远不会被合并。
|
|
187
187
|
|
|
188
|
+
#### 副作用导入
|
|
189
|
+
|
|
190
|
+
副作用导入(`import 'mod'`)的顺序通常有运行时语义,例如 CSS 的层叠顺序、polyfill 必须在框架之前加载等。插件不会跨越副作用导入移动其他 import 语句:
|
|
191
|
+
|
|
192
|
+
排序前:
|
|
193
|
+
|
|
194
|
+
<!-- prettier-ignore -->
|
|
195
|
+
```typescript
|
|
196
|
+
import Button from './Button';
|
|
197
|
+
import App from './App';
|
|
198
|
+
import 'normalize.css';
|
|
199
|
+
import theme from './theme';
|
|
200
|
+
import Icon from './Icon';
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
排序后:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import App from './App';
|
|
207
|
+
import Button from './Button';
|
|
208
|
+
|
|
209
|
+
import 'normalize.css';
|
|
210
|
+
|
|
211
|
+
import Icon from './Icon';
|
|
212
|
+
import theme from './theme';
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
副作用导入两侧的 import 各自独立排序,副作用导入本身保持原位不动。
|
|
216
|
+
|
|
188
217
|
排序规则:
|
|
189
218
|
|
|
190
219
|
- import 按分组分类,分组内按字母序排列
|
|
@@ -192,7 +221,7 @@ import { useEffect, useState } from 'react';
|
|
|
192
221
|
- 分组之间默认插入空行,可通过 `importOrderSeparation` 关闭
|
|
193
222
|
- `type` import 默认拆成独立语句,可通过 `importOrderTypeImports` 调整为内联
|
|
194
223
|
- 同一来源的多条 import 默认合并为一条,可通过 `importOrderMergeDuplicates` 关闭
|
|
195
|
-
- 副作用导入(`import 'mod'
|
|
224
|
+
- 副作用导入(`import 'mod'`)其顺序有语义,不会被移动,两侧的 import 各自独立排序
|
|
196
225
|
|
|
197
226
|
### export
|
|
198
227
|
|
package/dist/index.js
CHANGED
|
@@ -121,28 +121,7 @@ var options = {
|
|
|
121
121
|
}
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
// src/
|
|
125
|
-
function sortExports(text, rawOptions) {
|
|
126
|
-
const options2 = resolveSortOptions(rawOptions);
|
|
127
|
-
if (!options2.exportOrder) {
|
|
128
|
-
return text;
|
|
129
|
-
}
|
|
130
|
-
return text.replace(/export(\s+type)?\s*\{([^}]*)\}/g, (match, typeKeyword, inner) => {
|
|
131
|
-
const members = splitTopLevel(inner, ",");
|
|
132
|
-
if (members.length <= 1) {
|
|
133
|
-
return match;
|
|
134
|
-
}
|
|
135
|
-
const sorted = [...members].sort((a, b) => stripTypePrefix(a).localeCompare(stripTypePrefix(b), "en", {
|
|
136
|
-
sensitivity: "base"
|
|
137
|
-
}));
|
|
138
|
-
const same = sorted.every((m, i) => m === members[i]);
|
|
139
|
-
if (same) {
|
|
140
|
-
return match;
|
|
141
|
-
}
|
|
142
|
-
const prefix = typeKeyword ? `export${typeKeyword}` : "export";
|
|
143
|
-
return `${prefix} { ${sorted.join(", ")} }`;
|
|
144
|
-
});
|
|
145
|
-
}
|
|
124
|
+
// src/utils.ts
|
|
146
125
|
function splitTopLevel(input, separator) {
|
|
147
126
|
const out = [];
|
|
148
127
|
let buf = "";
|
|
@@ -165,6 +144,29 @@ function splitTopLevel(input, separator) {
|
|
|
165
144
|
}
|
|
166
145
|
return out.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
167
146
|
}
|
|
147
|
+
|
|
148
|
+
// src/sort-exports.ts
|
|
149
|
+
function sortExports(text, rawOptions) {
|
|
150
|
+
const options2 = resolveSortOptions(rawOptions);
|
|
151
|
+
if (!options2.exportOrder) {
|
|
152
|
+
return text;
|
|
153
|
+
}
|
|
154
|
+
return text.replace(/export(\s+type)?\s*\{([^}]*)\}/g, (match, typeKeyword, inner) => {
|
|
155
|
+
const members = splitTopLevel(inner, ",");
|
|
156
|
+
if (members.length <= 1) {
|
|
157
|
+
return match;
|
|
158
|
+
}
|
|
159
|
+
const sorted = [...members].sort((a, b) => stripTypePrefix(a).localeCompare(stripTypePrefix(b), "en", {
|
|
160
|
+
sensitivity: "base"
|
|
161
|
+
}));
|
|
162
|
+
const same = sorted.every((m, i) => m === members[i]);
|
|
163
|
+
if (same) {
|
|
164
|
+
return match;
|
|
165
|
+
}
|
|
166
|
+
const prefix = typeKeyword ? `export${typeKeyword}` : "export";
|
|
167
|
+
return `${prefix} { ${sorted.join(", ")} }`;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
168
170
|
function stripTypePrefix(member) {
|
|
169
171
|
return member.replace(/^type\s+/, "");
|
|
170
172
|
}
|
|
@@ -195,30 +197,8 @@ function detectGroup(source) {
|
|
|
195
197
|
}
|
|
196
198
|
return "external";
|
|
197
199
|
}
|
|
198
|
-
function splitTopLevel2(input, separator) {
|
|
199
|
-
const out = [];
|
|
200
|
-
let buf = "";
|
|
201
|
-
let depth = 0;
|
|
202
|
-
for (const ch of input) {
|
|
203
|
-
if (ch === "{" || ch === "(" || ch === "[") {
|
|
204
|
-
depth++;
|
|
205
|
-
} else if (ch === "}" || ch === ")" || ch === "]") {
|
|
206
|
-
depth--;
|
|
207
|
-
}
|
|
208
|
-
if (ch === separator && depth === 0) {
|
|
209
|
-
out.push(buf);
|
|
210
|
-
buf = "";
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
buf += ch;
|
|
214
|
-
}
|
|
215
|
-
if (buf.length > 0) {
|
|
216
|
-
out.push(buf);
|
|
217
|
-
}
|
|
218
|
-
return out.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
219
|
-
}
|
|
220
200
|
function splitMembers(inner) {
|
|
221
|
-
return
|
|
201
|
+
return splitTopLevel(inner, ",").map((part) => {
|
|
222
202
|
const isType = /^type\s+/.test(part);
|
|
223
203
|
const name = isType ? part.replace(/^type\s+/, "").trim() : part;
|
|
224
204
|
return { name, isType };
|
|
@@ -227,7 +207,7 @@ function splitMembers(inner) {
|
|
|
227
207
|
function parseImport(stmt) {
|
|
228
208
|
const trimmed = stmt.raw.trim();
|
|
229
209
|
const leadingComments = stmt.leadingComments;
|
|
230
|
-
const sideEffect = /^import\s*(['"])([^'"]+)\1\s*;?$/.exec(trimmed);
|
|
210
|
+
const sideEffect = /^import\s*(['"])([^'"]+)\1(?:\s+with\s*(\{[^}]*\}))?\s*;?$/.exec(trimmed);
|
|
231
211
|
if (sideEffect) {
|
|
232
212
|
return {
|
|
233
213
|
raw: trimmed,
|
|
@@ -237,20 +217,22 @@ function parseImport(stmt) {
|
|
|
237
217
|
defaultSpec: null,
|
|
238
218
|
namespaceSpec: null,
|
|
239
219
|
members: null,
|
|
220
|
+
attributes: sideEffect[3] ?? null,
|
|
240
221
|
leadingComments
|
|
241
222
|
};
|
|
242
223
|
}
|
|
243
|
-
const m = /^import\s+(type\s+)?([\s\S]+?)\s
|
|
224
|
+
const m = /^import\s+(type\s+)?([\s\S]+?)\s*from\s*(['"])([^'"]+)\3(?:\s+with\s*(\{[^}]*\}))?\s*;?$/.exec(trimmed);
|
|
244
225
|
if (!m) {
|
|
245
226
|
return null;
|
|
246
227
|
}
|
|
247
228
|
const typeClause = Boolean(m[1]);
|
|
248
229
|
const clause = (m[2] ?? "").trim();
|
|
249
230
|
const source = m[4] ?? "";
|
|
231
|
+
const attributes = m[5] ?? null;
|
|
250
232
|
let defaultSpec = null;
|
|
251
233
|
let namespaceSpec = null;
|
|
252
234
|
let members = null;
|
|
253
|
-
for (const part of
|
|
235
|
+
for (const part of splitTopLevel(clause, ",")) {
|
|
254
236
|
if (part.startsWith("{")) {
|
|
255
237
|
const inner = part.slice(1, part.lastIndexOf("}")).trim();
|
|
256
238
|
members = inner ? splitMembers(inner) : [];
|
|
@@ -268,6 +250,7 @@ function parseImport(stmt) {
|
|
|
268
250
|
defaultSpec,
|
|
269
251
|
namespaceSpec,
|
|
270
252
|
members,
|
|
253
|
+
attributes,
|
|
271
254
|
leadingComments
|
|
272
255
|
};
|
|
273
256
|
}
|
|
@@ -285,7 +268,7 @@ function extractImportBlock(text) {
|
|
|
285
268
|
const chunkMatch = /^(?:[ \t]*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)?[ \t]*\n)*/.exec(text.slice(cursor));
|
|
286
269
|
const chunk = chunkMatch ? chunkMatch[0] : "";
|
|
287
270
|
const afterSkip = cursor + chunk.length;
|
|
288
|
-
const importMatch = /^[ \t]*(import\b[\s\S]*?(?:from\s*(['"])[^'"]+\2|(['"])[^'"]+\3)\s*;?)/.exec(text.slice(afterSkip));
|
|
271
|
+
const importMatch = /^[ \t]*(import\b[\s\S]*?(?:from\s*(['"])[^'"]+\2|(['"])[^'"]+\3)(?:\s+with\s*\{[^}]*\})?\s*;?)/.exec(text.slice(afterSkip));
|
|
289
272
|
if (!importMatch) {
|
|
290
273
|
break;
|
|
291
274
|
}
|
|
@@ -323,13 +306,14 @@ function renderMembers(members) {
|
|
|
323
306
|
return members.map((member) => member.isType ? `type ${member.name}` : member.name).join(", ");
|
|
324
307
|
}
|
|
325
308
|
function renderImport(importDecl) {
|
|
309
|
+
const suffix = importDecl.attributes ? ` with ${importDecl.attributes}` : "";
|
|
326
310
|
const body = (() => {
|
|
327
311
|
if (importDecl.sideEffect) {
|
|
328
|
-
return `import '${importDecl.source}';`;
|
|
312
|
+
return `import '${importDecl.source}'${suffix};`;
|
|
329
313
|
}
|
|
330
314
|
if (importDecl.typeClause) {
|
|
331
315
|
const inner = importDecl.members ? `{ ${renderMembers(importDecl.members)} }` : "";
|
|
332
|
-
return `import type ${inner} from '${importDecl.source}';`;
|
|
316
|
+
return `import type ${inner} from '${importDecl.source}'${suffix};`;
|
|
333
317
|
}
|
|
334
318
|
const leftParts = [];
|
|
335
319
|
if (importDecl.defaultSpec) {
|
|
@@ -341,7 +325,7 @@ function renderImport(importDecl) {
|
|
|
341
325
|
if (importDecl.members) {
|
|
342
326
|
leftParts.push(`{ ${renderMembers(importDecl.members)} }`);
|
|
343
327
|
}
|
|
344
|
-
return `import ${leftParts.join(", ")} from '${importDecl.source}';`;
|
|
328
|
+
return `import ${leftParts.join(", ")} from '${importDecl.source}'${suffix};`;
|
|
345
329
|
})();
|
|
346
330
|
return importDecl.leadingComments + body;
|
|
347
331
|
}
|
|
@@ -382,6 +366,7 @@ function mergeImportsFromSameSource(imports) {
|
|
|
382
366
|
defaultSpec: existing.defaultSpec ?? importDecl.defaultSpec,
|
|
383
367
|
namespaceSpec: existing.namespaceSpec ?? importDecl.namespaceSpec,
|
|
384
368
|
members: existing.members === null && importDecl.members === null ? null : [...existing.members ?? [], ...importDecl.members ?? []],
|
|
369
|
+
attributes: existing.attributes ?? importDecl.attributes,
|
|
385
370
|
leadingComments: existing.leadingComments
|
|
386
371
|
};
|
|
387
372
|
}
|
|
@@ -407,6 +392,7 @@ function applyTypeImports(importDecl, style) {
|
|
|
407
392
|
defaultSpec: null,
|
|
408
393
|
namespaceSpec: null,
|
|
409
394
|
members: sortMembersAlpha(typeMembers2.map((member) => ({ ...member, isType: false }))),
|
|
395
|
+
attributes: importDecl.attributes,
|
|
410
396
|
leadingComments: importDecl.leadingComments
|
|
411
397
|
});
|
|
412
398
|
}
|
|
@@ -438,27 +424,13 @@ function applyTypeImports(importDecl, style) {
|
|
|
438
424
|
const ordered = style === "inline-first" ? [...sortedTypes, ...sortedValues] : [...sortedValues, ...sortedTypes];
|
|
439
425
|
return [{ ...base, members: ordered }];
|
|
440
426
|
}
|
|
441
|
-
function
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return text;
|
|
445
|
-
}
|
|
446
|
-
const block = extractImportBlock(text);
|
|
447
|
-
if (!block || block.statements.length === 0) {
|
|
448
|
-
return text;
|
|
449
|
-
}
|
|
450
|
-
const parsed = block.statements.map((rawStmt) => parseImport(rawStmt)).filter((decl) => decl !== null);
|
|
451
|
-
if (parsed.length === 0) {
|
|
452
|
-
return text;
|
|
427
|
+
function sortSegment(imports, options2, groupIndex, fallback) {
|
|
428
|
+
if (imports.length === 0) {
|
|
429
|
+
return [];
|
|
453
430
|
}
|
|
454
431
|
const style = options2.importOrderTypeImports;
|
|
455
|
-
const deduped = options2.importOrderMergeDuplicates ? mergeImportsFromSameSource(
|
|
432
|
+
const deduped = options2.importOrderMergeDuplicates ? mergeImportsFromSameSource(imports) : imports;
|
|
456
433
|
const rewritten = deduped.flatMap((importDecl) => applyTypeImports(importDecl, style));
|
|
457
|
-
const groupIndex = new Map(options2.importOrderGroups.map((group, index) => [
|
|
458
|
-
group,
|
|
459
|
-
index
|
|
460
|
-
]));
|
|
461
|
-
const fallback = options2.importOrderGroups.length;
|
|
462
434
|
const decorated = rewritten.map((importDecl, index) => ({
|
|
463
435
|
stmt: importDecl,
|
|
464
436
|
group: detectGroup(importDecl.source),
|
|
@@ -489,9 +461,63 @@ function sortImports(text, rawOptions) {
|
|
|
489
461
|
lines.push(renderImport(item.stmt));
|
|
490
462
|
prevGroup = item.group;
|
|
491
463
|
}
|
|
492
|
-
|
|
464
|
+
return lines;
|
|
465
|
+
}
|
|
466
|
+
function sortImports(text, rawOptions) {
|
|
467
|
+
const options2 = resolveSortOptions(rawOptions);
|
|
468
|
+
if (!options2.importOrder) {
|
|
469
|
+
return text;
|
|
470
|
+
}
|
|
471
|
+
const block = extractImportBlock(text);
|
|
472
|
+
if (!block || block.statements.length === 0) {
|
|
473
|
+
return text;
|
|
474
|
+
}
|
|
475
|
+
const parsed = block.statements.map((rawStmt) => parseImport(rawStmt)).filter((decl) => decl !== null);
|
|
476
|
+
if (parsed.length === 0) {
|
|
477
|
+
return text;
|
|
478
|
+
}
|
|
479
|
+
const groupIndex = new Map(options2.importOrderGroups.map((group, index) => [
|
|
480
|
+
group,
|
|
481
|
+
index
|
|
482
|
+
]));
|
|
483
|
+
const fallback = options2.importOrderGroups.length;
|
|
484
|
+
const chunks = [];
|
|
485
|
+
let currentSegment = [];
|
|
486
|
+
for (const importDecl of parsed) {
|
|
487
|
+
if (importDecl.sideEffect) {
|
|
488
|
+
chunks.push({ kind: "segment", imports: currentSegment });
|
|
489
|
+
chunks.push({ kind: "side-effect", stmt: importDecl });
|
|
490
|
+
currentSegment = [];
|
|
491
|
+
} else {
|
|
492
|
+
currentSegment.push(importDecl);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
chunks.push({ kind: "segment", imports: currentSegment });
|
|
496
|
+
const allLines = [];
|
|
497
|
+
for (const chunk of chunks) {
|
|
498
|
+
if (chunk.kind === "segment") {
|
|
499
|
+
if (chunk.imports.length === 0) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const segmentLines = sortSegment(chunk.imports, options2, groupIndex, fallback);
|
|
503
|
+
if (allLines.length > 0 && options2.importOrderSeparation) {
|
|
504
|
+
allLines.push("");
|
|
505
|
+
}
|
|
506
|
+
allLines.push(...segmentLines);
|
|
507
|
+
} else {
|
|
508
|
+
if (allLines.length > 0 && options2.importOrderSeparation) {
|
|
509
|
+
allLines.push("");
|
|
510
|
+
}
|
|
511
|
+
allLines.push(renderImport(chunk.stmt));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const replacement = allLines.join(`
|
|
493
515
|
`);
|
|
494
|
-
|
|
516
|
+
const trailing = text.slice(block.end);
|
|
517
|
+
const suffix = trailing.trim() ? `
|
|
518
|
+
|
|
519
|
+
` + trailing.trimStart() : trailing;
|
|
520
|
+
return text.slice(0, block.start) + replacement + suffix;
|
|
495
521
|
}
|
|
496
522
|
|
|
497
523
|
// src/sort-package.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prettier-plugin-sort",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "An all-in-one Prettier plugin to sort imports, package.json keys, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"imports",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"prettier-plugin",
|
|
13
13
|
"sort"
|
|
14
14
|
],
|
|
15
|
-
"homepage": "https://github.com/xueelf/
|
|
15
|
+
"homepage": "https://github.com/xueelf/prettier-plugin-sort",
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/xueelf/
|
|
17
|
+
"url": "https://github.com/xueelf/prettier-plugin-sort/issues"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/xueelf/
|
|
21
|
+
"url": "git+https://github.com/xueelf/prettier-plugin-sort.git"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"author": "Yuki <admin@yuki.sh>",
|