prettier-plugin-sort 0.0.2 → 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 CHANGED
@@ -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 merged or moved across groups
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/sort-exports.ts
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 splitTopLevel2(inner, ",").map((part) => {
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*from\s*(['"])([^'"]+)\3\s*;?$/.exec(trimmed);
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 splitTopLevel2(clause, ",")) {
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 sortImports(text, rawOptions) {
442
- const options2 = resolveSortOptions(rawOptions);
443
- if (!options2.importOrder) {
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(parsed) : parsed;
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,7 +461,57 @@ function sortImports(text, rawOptions) {
489
461
  lines.push(renderImport(item.stmt));
490
462
  prevGroup = item.group;
491
463
  }
492
- const replacement = lines.join(`
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);
495
517
  const suffix = trailing.trim() ? `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-sort",
3
- "version": "0.0.2",
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",