prettier-plugin-sort 0.1.0 → 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
@@ -56,7 +56,7 @@ import react from 'react';
56
56
  import App from './App.tsx';
57
57
  ```
58
58
 
59
- Each import belongs to a group. For example, `node:fs` is a `builtin` (covers Node.js / Bun / Deno built-ins), while `react` and `lodash` are `external` npm packages. The plugin first classifies each import by group, then sorts alphabetically within each group.
59
+ Each import belongs to a group. For example, `node:fs` is a `builtin` (covers Node.js and Bun **runtime** built-ins), while `react` and `lodash` are `external` npm packages. The plugin first classifies each import by group, then sorts alphabetically within each group.
60
60
 
61
61
  Grouping and ordering follow the conventions of [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import)'s [import/order](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md) rule.
62
62
 
@@ -76,7 +76,7 @@ Group matchers:
76
76
 
77
77
  | Group | Matches | Examples |
78
78
  | ---------- | ----------------------------------------------------------- | ---------------------------------- |
79
- | `builtin` | `node:*`, `bun:*`, `deno:*`, and unprefixed Node built-ins | `node:fs`, `path` |
79
+ | `builtin` | `node:*`, `bun:*`, `bun`, and unprefixed Node built-ins | `node:fs`, `path`, `bun` |
80
80
  | `external` | npm packages, and anything that doesn't match another group | `react`, `@scope/pkg` |
81
81
  | `internal` | Project absolute paths and aliases | `/utils`, `~/app`, `@/shared` |
82
82
  | `parent` | Parent-relative paths | `../Button` |
@@ -283,9 +283,10 @@ Top-level key order follows the field list maintained by [sort-package-json](htt
283
283
  Rules the plugin follows:
284
284
 
285
285
  - Top-level keys are reordered to the canonical sequence (`name` → `version` → ... → `dependencies`)
286
- - Top-level string-only arrays are sorted alphabetically, e.g. `keywords`, `files`
286
+ - `scripts` and `betterScripts` keys are sorted with pre/post lifecycle grouping and `:` namespace ordering
287
+ - `exports` keys are sorted: path subkeys first, conditions alphabetically with `default` moved to the end; nested export maps are recursively sorted
287
288
  - `dependencies`, `devDependencies`, `peerDependencies` and other dependency maps are always sorted alphabetically, even if `packageJsonOrder` is set to `false`, because `npm install` rewrites them in alphabetical order every time
288
- - `scripts`, `exports`, `imports` and other nested objects are not sorted recursively, because their key order carries runtime semantics
289
+ - String arrays are handled by field: `keywords`, `files`, `activationEvents` are deduplicated preserving original order; `bundledDependencies`, `bundleDependencies`, `extensionPack`, `extensionDependencies` are deduplicated then sorted; `workspaces` arrays are left untouched; all other string arrays are sorted alphabetically
289
290
  - Use `packageJsonOrderExcludeKeys` to opt specific top-level keys out of sorting entirely
290
291
 
291
292
  ## Options
@@ -300,7 +301,7 @@ Prettier plugin options are flat, so these options are prefixed with `importOrde
300
301
  | `importOrderTypeImports` | How to place `type` imports: `separate`, `inline-first`, `inline-last`, `mixed` | `"separate"` |
301
302
  | `importOrderMergeDuplicates` | Merge multiple `import` statements from the same source (except side-effect imports) | `true` |
302
303
  | `exportOrder` | Sort named specifiers inside `export { … }` alphabetically | `true` |
303
- | `packageJsonOrder` | Sort top-level keys and string arrays in `package.json` | `true` |
304
+ | `packageJsonOrder` | Sort top-level keys, scripts/exports subkeys, and string arrays in `package.json` | `true` |
304
305
  | `packageJsonOrderExcludeKeys` | Top-level `package.json` keys to leave untouched | `[]` |
305
306
 
306
307
  ## Example
package/README.zh.md CHANGED
@@ -56,7 +56,7 @@ import react from 'react';
56
56
  import App from './App.tsx';
57
57
  ```
58
58
 
59
- import 的模块一般可以按其来源分为不同的类别,例如上面的示例代码,'node:fs' 就属于 builtin 分类,该分类涵盖 Node.jsBun、Deno 等**运行时**的内置模块。而像 react 和 lodash 这种直接从 npm 下载的依赖模块,属于 external 分类。
59
+ import 的模块一般可以按其来源分为不同的类别,例如上面的示例代码,'node:fs' 就属于 builtin 分类,该分类涵盖 Node.jsBun **运行时**的内置模块。而像 react 和 lodash 这种直接从 npm 下载的依赖模块,属于 external 分类。
60
60
 
61
61
  插件会先根据不同的模块进行分类,然后在模块内部基于字母顺序进行排列。
62
62
 
@@ -76,14 +76,14 @@ import 的模块一般可以按其来源分为不同的类别,例如上面的
76
76
 
77
77
  各分组的匹配规则如下:
78
78
 
79
- | 分组 | 匹配内容 | 示例 |
80
- | ---------- | ------------------------------------------------------- | ---------------------------------- |
81
- | `builtin` | `node:*`、`bun:*`、`deno:*`,以及无前缀的 Node 内置模块 | `node:fs`、`path` |
82
- | `external` | npm 包,以及不属于其他分组的模块 | `react`、`@scope/pkg` |
83
- | `internal` | 项目绝对路径与别名 | `/utils`、`~/app`、`@/shared` |
84
- | `parent` | 向上跳级的相对路径 | `../Button` |
85
- | `sibling` | 同级相对路径(不包含 index) | `./Icon` |
86
- | `index` | 当前目录的 index 模块 | `.`、`./`、`./index`、`./index.ts` |
79
+ | 分组 | 匹配内容 | 示例 |
80
+ | ---------- | ---------------------------------------------------- | ---------------------------------- |
81
+ | `builtin` | `node:*`、`bun:*`、`bun`,以及无前缀的 Node 内置模块 | `node:fs`、`path`、`bun` |
82
+ | `external` | npm 包,以及不属于其他分组的模块 | `react`、`@scope/pkg` |
83
+ | `internal` | 项目绝对路径与别名 | `/utils`、`~/app`、`@/shared` |
84
+ | `parent` | 向上跳级的相对路径 | `../Button` |
85
+ | `sibling` | 同级相对路径(不包含 index) | `./Icon` |
86
+ | `index` | 当前目录的 index 模块 | `.`、`./`、`./index`、`./index.ts` |
87
87
 
88
88
  > **关于 `internal` 的检测方式:** 插件目前通过说明符的前缀硬编码(`/`、`~`、`@/`)来判断,不会读取 tsconfig `paths` 或任何构建工具的配置。后续版本可能会提供 `importOrderInternalPatterns` 选项,支持自定义正则匹配。
89
89
 
@@ -286,9 +286,10 @@ export { type FC, useEffect, useState } from 'react';
286
286
  排序规则:
287
287
 
288
288
  - 顶层字段按常用顺序排列(`name` → `version` → ... → `dependencies`)
289
- - 顶层的纯字符串数组按字母序排列,如 `keywords`、`files`
289
+ - `scripts` `betterScripts` 的键名按 pre/post 生命周期包围和 `:` 命名空间分组排序
290
+ - `exports` 键按路径优先、条件字母序、`default` 置底排序,嵌套的 export 对象递归处理
290
291
  - `dependencies`、`devDependencies`、`peerDependencies` 等依赖表永远按字母序排列,即使 `packageJsonOrder` 设为 `false` 也不例外。因为 `npm install` 每次都会按照字母序写回
291
- - `scripts`、`exports`、`imports` 等嵌套对象不会递归排序,它们的键顺序有运行时语义
292
+ - 字符串数组按字段分类处理:`keywords`、`files`、`activationEvents` 只去重不排序,保留原始语义顺序;`bundledDependencies`、`bundleDependencies`、`extensionPack`、`extensionDependencies` 去重后按字母序排列;`workspaces` 数组不排序;其余字符串数组按字母序排列
292
293
  - 想让某些顶层字段完全跳过排序,可以在 `packageJsonOrderExcludeKeys` 里列出
293
294
 
294
295
  ## 配置项
@@ -303,7 +304,7 @@ Prettier 的插件选项是扁平的,所以这些配置都以 `importOrder`、
303
304
  | `importOrderTypeImports` | `type` import 的处理方式:`separate`、`inline-first`、`inline-last`、`mixed` | `"separate"` |
304
305
  | `importOrderMergeDuplicates` | 是否合并同来源的多条 import 语句(副作用导入除外) | `true` |
305
306
  | `exportOrder` | 是否按字母序排列 `export { … }` 花括号内的命名导出 | `true` |
306
- | `packageJsonOrder` | 是否排序 package.json 的顶层字段和字符串数组 | `true` |
307
+ | `packageJsonOrder` | 是否排序 package.json 的顶层字段、scripts/exports 子键和字符串数组 | `true` |
307
308
  | `packageJsonOrderExcludeKeys` | 不参与 package.json 排序的顶层字段 | `[]` |
308
309
 
309
310
  ## 示例
package/dist/index.d.ts CHANGED
@@ -1,28 +1,5 @@
1
- // Generated by dts-bundle-generator v9.5.1
2
-
3
- import { Plugin, SupportOptions } from 'prettier';
4
-
5
- /** import 分组,参考 eslint-plugin-import 的 import/order 规则。 */
6
- export type ImportGroup = "builtin" | "external" | "internal" | "parent" | "sibling" | "index";
7
- /** `import type` 内联风格,参考 @typescript-eslint/consistent-type-imports 的 fixStyle 选项。 */
8
- export type TypeImportsStyle = "separate" | "inline-first" | "inline-last" | "mixed";
9
- /** 插件排序配置。 */
10
- export interface SortOptions {
11
- importOrder?: boolean;
12
- importOrderGroups?: ImportGroup[];
13
- importOrderSeparation?: boolean;
14
- importOrderTypeImports?: TypeImportsStyle;
15
- importOrderMergeDuplicates?: boolean;
16
- exportOrder?: boolean;
17
- packageJsonOrder?: boolean;
18
- packageJsonOrderExcludeKeys?: string[];
19
- }
20
- /** 向 Prettier 注册的选项。使用功能名词前缀命名,因为 Prettier API 不支持嵌套选项对象。 */
21
- export declare const options: SupportOptions;
1
+ import { type Plugin } from 'prettier';
22
2
  declare const plugin: Plugin;
23
-
24
- export {
25
- plugin as default,
26
- };
27
-
28
- export {};
3
+ export default plugin;
4
+ export { options } from './options';
5
+ export type { ImportGroup, SortOptions, TypeImportsStyle } from './options';
package/dist/index.js CHANGED
@@ -172,8 +172,9 @@ function stripTypePrefix(member) {
172
172
  // src/sort-imports.ts
173
173
  import { builtinModules } from "node:module";
174
174
  var NODE_BUILTINS = new Set(builtinModules);
175
+ var INDEX_PATTERN = /^\.\/index(\.[a-z]+)?$/;
175
176
  function detectGroup(source) {
176
- if (source.startsWith("node:") || source.startsWith("bun:") || source.startsWith("deno:")) {
177
+ if (source === "bun" || source.startsWith("bun:") || source.startsWith("node:")) {
177
178
  return "builtin";
178
179
  }
179
180
  const slashIndex = source.indexOf("/");
@@ -181,7 +182,7 @@ function detectGroup(source) {
181
182
  if (head && NODE_BUILTINS.has(head)) {
182
183
  return "builtin";
183
184
  }
184
- if (source === "." || source === "./" || /^\.\/index(\.[a-z]+)?$/.test(source)) {
185
+ if (source === "." || source === "./" || INDEX_PATTERN.test(source)) {
185
186
  return "index";
186
187
  }
187
188
  if (source.startsWith("../") || source === "..") {
@@ -195,7 +196,7 @@ function detectGroup(source) {
195
196
  }
196
197
  return "external";
197
198
  }
198
- var TYPE_PREFIX = /^type\s+(.+)$/s;
199
+ var TYPE_PREFIX = /^type\s+(.+)$/;
199
200
  function splitMembers(inner) {
200
201
  return splitTopLevel(inner, ",").map((part) => {
201
202
  const match = TYPE_PREFIX.exec(part);
@@ -251,7 +252,7 @@ function parseImport(statement) {
251
252
  };
252
253
  }
253
254
  function extractImportBlock(text) {
254
- const firstRe = /(?:^|\n)(?:[ \t]*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)[ \t]*\n)*[ \t]*import\b/;
255
+ const firstRe = /(?:^|\n)(?:[ \t]*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)[ \t]*\n)*[ \t]*import\b(?![.(])/;
255
256
  const first = firstRe.exec(text);
256
257
  if (!first) {
257
258
  return null;
@@ -260,7 +261,7 @@ function extractImportBlock(text) {
260
261
  ` ? 1 : 0);
261
262
  const statements = [];
262
263
  const skipRe = /(?:[ \t]*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)?[ \t]*\n)*/y;
263
- const importRe = /[ \t]*(import\b[\s\S]*?(?:from\s*(['"])[^'"]+\2|(['"])[^'"]+\3)(?:\s+with\s*\{[^}]*\})?\s*;?)/y;
264
+ const importRe = /[ \t]*(import\b(?![.(])[\s\S]*?(?:from\s*(['"])[^'"]+\2|(['"])[^'"]+\3)(?:\s+with\s*\{[^}]*\})?\s*;?)/y;
264
265
  let cursor = start;
265
266
  while (cursor < text.length) {
266
267
  skipRe.lastIndex = cursor;
@@ -279,9 +280,6 @@ function extractImportBlock(text) {
279
280
  const leadingLines = [];
280
281
  for (let i = commentLines.length - 1;i >= 0; i--) {
281
282
  const line = commentLines[i];
282
- if (line === undefined) {
283
- continue;
284
- }
285
283
  if (line.trim() === "") {
286
284
  break;
287
285
  }
@@ -338,7 +336,7 @@ function normalizeTypeClause(importDecl) {
338
336
  };
339
337
  }
340
338
  function mergeImportsFromSameSource(imports) {
341
- const indexBySource = new Map;
339
+ const indexByKey = new Map;
342
340
  const result = [];
343
341
  for (const rawImport of imports) {
344
342
  const importDecl = normalizeTypeClause(rawImport);
@@ -346,9 +344,10 @@ function mergeImportsFromSameSource(imports) {
346
344
  result.push(importDecl);
347
345
  continue;
348
346
  }
349
- const existingIndex = indexBySource.get(importDecl.source);
347
+ const mergeKey = `${importDecl.source}\x00${importDecl.attributes ?? ""}`;
348
+ const existingIndex = indexByKey.get(mergeKey);
350
349
  if (existingIndex === undefined) {
351
- indexBySource.set(importDecl.source, result.length);
350
+ indexByKey.set(mergeKey, result.length);
352
351
  result.push(importDecl);
353
352
  continue;
354
353
  }
@@ -360,7 +359,7 @@ function mergeImportsFromSameSource(imports) {
360
359
  defaultSpec: existing.defaultSpec ?? importDecl.defaultSpec,
361
360
  namespaceSpec: existing.namespaceSpec ?? importDecl.namespaceSpec,
362
361
  members: existing.members === null && importDecl.members === null ? null : [...existing.members ?? [], ...importDecl.members ?? []],
363
- attributes: existing.attributes ?? importDecl.attributes,
362
+ attributes: existing.attributes,
364
363
  leadingComments: existing.leadingComments
365
364
  };
366
365
  }
@@ -376,9 +375,9 @@ function applyTypeImports(importDecl, style) {
376
375
  }
377
376
  const typeMembers2 = importDecl.members.filter((member) => member.isType);
378
377
  const valueMembers2 = importDecl.members.filter((member) => !member.isType);
379
- const out = [];
378
+ const output = [];
380
379
  if (typeMembers2.length > 0) {
381
- out.push({
380
+ output.push({
382
381
  source: importDecl.source,
383
382
  typeClause: true,
384
383
  sideEffect: false,
@@ -391,13 +390,13 @@ function applyTypeImports(importDecl, style) {
391
390
  }
392
391
  const hasValueBody = valueMembers2.length > 0 || importDecl.defaultSpec !== null || importDecl.namespaceSpec !== null;
393
392
  if (hasValueBody) {
394
- out.push({
393
+ output.push({
395
394
  ...importDecl,
396
395
  members: valueMembers2.length > 0 ? sortMembersAlpha(valueMembers2) : null,
397
396
  leadingComments: typeMembers2.length > 0 ? "" : importDecl.leadingComments
398
397
  });
399
398
  }
400
- return out.length > 0 ? out : [importDecl];
399
+ return output.length > 0 ? output : [importDecl];
401
400
  }
402
401
  const inlineBase = importDecl.typeClause ? {
403
402
  ...importDecl,
@@ -422,8 +421,8 @@ function sortSegment(imports, options2, groupIndex, fallback) {
422
421
  return [];
423
422
  }
424
423
  const style = options2.importOrderTypeImports;
425
- const deduped = options2.importOrderMergeDuplicates ? mergeImportsFromSameSource(imports) : imports;
426
- const rewritten = deduped.flatMap((importDecl) => applyTypeImports(importDecl, style));
424
+ const deduplicated = options2.importOrderMergeDuplicates ? mergeImportsFromSameSource(imports) : imports;
425
+ const rewritten = deduplicated.flatMap((importDecl) => applyTypeImports(importDecl, style));
427
426
  const decorated = rewritten.map((importDecl, index) => ({
428
427
  importDecl,
429
428
  group: detectGroup(importDecl.source),
@@ -506,9 +505,10 @@ function sortImports(text, rawOptions) {
506
505
  const replacement = allLines.join(`
507
506
  `);
508
507
  const trailing = text.slice(block.end);
509
- const suffix = trailing.trim() ? `
508
+ const nonBlankIndex = trailing.search(/\S/);
509
+ const suffix = nonBlankIndex >= 0 ? `
510
510
 
511
- ` + trailing.trimStart() : trailing;
511
+ ` + trailing.slice(nonBlankIndex) : trailing;
512
512
  return text.slice(0, block.start) + replacement + suffix;
513
513
  }
514
514
 
@@ -633,8 +633,6 @@ var DEPENDENCY_FIELDS = [
633
633
  "devDependencies",
634
634
  "peerDependencies",
635
635
  "optionalDependencies",
636
- "bundledDependencies",
637
- "bundleDependencies",
638
636
  "resolutions",
639
637
  "overrides"
640
638
  ];
@@ -646,8 +644,8 @@ function isPlainObject(value) {
646
644
  function isStringArray(value) {
647
645
  return Array.isArray(value) && value.every((item) => typeof item === "string");
648
646
  }
649
- function sortObjectKeysByOrder(record, order) {
650
- const orderIndex = new Map(order.map((key, index) => [key, index]));
647
+ var TOP_LEVEL_ORDER_INDEX = new Map(PACKAGE_JSON_TOP_LEVEL_ORDER.map((key, index) => [key, index]));
648
+ function sortObjectKeysByOrder(record, orderIndex) {
651
649
  const known = [];
652
650
  const rest = [];
653
651
  for (const entry of Object.entries(record)) {
@@ -671,6 +669,9 @@ function sortObjectKeysAlpha(value) {
671
669
  function sortStringArrayAlpha(value) {
672
670
  return [...value].sort((a, b) => a.localeCompare(b, "en"));
673
671
  }
672
+ function uniqueStringArray(value) {
673
+ return [...new Set(value)];
674
+ }
674
675
  function isPackageJson(filepath) {
675
676
  if (!filepath) {
676
677
  return false;
@@ -681,6 +682,116 @@ function detectIndent(source) {
681
682
  const match = /\n([ \t]+)\S/.exec(source);
682
683
  return match ? match[1] ?? " " : " ";
683
684
  }
685
+ var DEFAULT_NPM_SCRIPTS = new Set([
686
+ "install",
687
+ "pack",
688
+ "prepare",
689
+ "publish",
690
+ "restart",
691
+ "shrinkwrap",
692
+ "start",
693
+ "stop",
694
+ "test",
695
+ "uninstall",
696
+ "version"
697
+ ]);
698
+ var RUN_S_PATTERN = /(?<=^|[\s&;<>|(])(?:run-s|npm-run-all2? .*(?:--sequential|--serial|-s))(?=$|[\s&;<>|)])/;
699
+ function hasSequentialScript(packageObject) {
700
+ const devDependencies = packageObject["devDependencies"];
701
+ if (!isPlainObject(devDependencies) || !Object.hasOwn(devDependencies, "npm-run-all") && !Object.hasOwn(devDependencies, "npm-run-all2")) {
702
+ return false;
703
+ }
704
+ const scriptCommands = ["scripts", "betterScripts"].flatMap((field) => {
705
+ const scriptsObject = packageObject[field];
706
+ return isPlainObject(scriptsObject) ? Object.values(scriptsObject) : [];
707
+ });
708
+ return scriptCommands.some((script) => typeof script === "string" && script.includes("*") && RUN_S_PATTERN.test(script));
709
+ }
710
+ function sortScriptNames(keys, prefix = "") {
711
+ const groupMap = new Map;
712
+ for (const key of keys) {
713
+ const rest = prefix ? key.slice(prefix.length + 1) : key;
714
+ const colonIndex = rest.indexOf(":");
715
+ if (colonIndex > 0) {
716
+ const base = key.slice(0, (prefix ? prefix.length + 1 : 0) + colonIndex);
717
+ let group = groupMap.get(base);
718
+ if (!group) {
719
+ group = [];
720
+ groupMap.set(base, group);
721
+ }
722
+ group.push(key);
723
+ } else {
724
+ let group = groupMap.get(key);
725
+ if (!group) {
726
+ group = [];
727
+ groupMap.set(key, group);
728
+ }
729
+ group.push(key);
730
+ }
731
+ }
732
+ return [...groupMap.keys()].sort((a, b) => a.localeCompare(b, "en")).flatMap((groupKey) => {
733
+ const children = groupMap.get(groupKey);
734
+ if (children.length > 1 && children.some((key) => key !== groupKey && key.startsWith(groupKey + ":"))) {
735
+ const direct = children.filter((key) => key === groupKey || !key.startsWith(groupKey + ":")).sort((a, b) => a.localeCompare(b, "en"));
736
+ const nested = children.filter((key) => key.startsWith(groupKey + ":"));
737
+ return [...direct, ...sortScriptNames(nested, groupKey)];
738
+ }
739
+ return children.sort((a, b) => a.localeCompare(b, "en"));
740
+ });
741
+ }
742
+ function sortScripts(scripts, packageObject) {
743
+ const names = Object.keys(scripts);
744
+ const prefixable = new Set;
745
+ const normalized = names.map((name) => {
746
+ const base = name.replace(/^(?:pre|post)/, "");
747
+ if (DEFAULT_NPM_SCRIPTS.has(base) || names.includes(base)) {
748
+ prefixable.add(base);
749
+ return base;
750
+ }
751
+ return name;
752
+ });
753
+ let sortedNames;
754
+ if (hasSequentialScript(packageObject)) {
755
+ sortedNames = [...new Set(normalized)];
756
+ } else {
757
+ sortedNames = sortScriptNames(normalized);
758
+ }
759
+ const orderedNames = sortedNames.flatMap((key) => prefixable.has(key) ? [`pre${key}`, key, `post${key}`] : [key]);
760
+ return Object.fromEntries(orderedNames.filter((name) => Object.hasOwn(scripts, name)).map((name) => [name, scripts[name]]));
761
+ }
762
+ function sortExportsField(exports) {
763
+ const keys = Object.keys(exports);
764
+ const paths = keys.filter((key) => key.startsWith(".")).sort();
765
+ const conditions = keys.filter((key) => !key.startsWith(".")).sort();
766
+ const defaultIndex = conditions.indexOf("default");
767
+ if (defaultIndex >= 0) {
768
+ conditions.splice(defaultIndex, 1);
769
+ conditions.push("default");
770
+ }
771
+ const orderedKeys = [...paths, ...conditions];
772
+ return Object.fromEntries(orderedKeys.map((key) => {
773
+ const value = exports[key];
774
+ return [
775
+ key,
776
+ isPlainObject(value) ? sortExportsField(value) : value
777
+ ];
778
+ }));
779
+ }
780
+ var UNIQUE_ONLY_FIELDS = new Set(["keywords", "files", "activationEvents"]);
781
+ var UNIQUE_AND_SORT_FIELDS = new Set([
782
+ "bundledDependencies",
783
+ "bundleDependencies",
784
+ "extensionPack",
785
+ "extensionDependencies"
786
+ ]);
787
+ var NO_SORT_ARRAY_FIELDS = new Set(["workspaces"]);
788
+ function detectNewline(source) {
789
+ const crlfIndex = source.indexOf(`\r
790
+ `);
791
+ return crlfIndex >= 0 ? `\r
792
+ ` : `
793
+ `;
794
+ }
684
795
  function sortPackageJson(text, rawOptions) {
685
796
  if (!isPackageJson(rawOptions.filepath)) {
686
797
  return text;
@@ -704,21 +815,51 @@ function sortPackageJson(text, rawOptions) {
704
815
  }
705
816
  }
706
817
  if (options2.packageJsonOrder) {
707
- result = sortObjectKeysByOrder(result, PACKAGE_JSON_TOP_LEVEL_ORDER);
818
+ result = sortObjectKeysByOrder(result, TOP_LEVEL_ORDER_INDEX);
819
+ for (const field of ["scripts", "betterScripts"]) {
820
+ const scriptsValue = result[field];
821
+ if (scriptsValue !== undefined && isPlainObject(scriptsValue) && !exclude.has(field)) {
822
+ result[field] = sortScripts(scriptsValue, result);
823
+ }
824
+ }
825
+ const exportsValue = result["exports"];
826
+ if (exportsValue !== undefined && isPlainObject(exportsValue) && !exclude.has("exports")) {
827
+ result["exports"] = sortExportsField(exportsValue);
828
+ }
708
829
  for (const [key, value] of Object.entries(result)) {
709
830
  if (exclude.has(key)) {
710
831
  continue;
711
832
  }
712
- if (isStringArray(value)) {
713
- result[key] = sortStringArrayAlpha(value);
833
+ if (!isStringArray(value)) {
834
+ continue;
835
+ }
836
+ if (NO_SORT_ARRAY_FIELDS.has(key)) {
837
+ continue;
838
+ }
839
+ if (UNIQUE_ONLY_FIELDS.has(key)) {
840
+ result[key] = uniqueStringArray(value);
841
+ continue;
842
+ }
843
+ if (UNIQUE_AND_SORT_FIELDS.has(key)) {
844
+ result[key] = sortStringArrayAlpha(uniqueStringArray(value));
845
+ continue;
714
846
  }
847
+ result[key] = sortStringArrayAlpha(value);
715
848
  }
716
849
  }
717
850
  const indent = detectIndent(text);
851
+ const newline = detectNewline(text);
718
852
  const output = JSON.stringify(result, null, indent);
719
- return text.endsWith(`
720
- `) ? output + `
721
- ` : output;
853
+ if (text.endsWith(newline)) {
854
+ return newline === `\r
855
+ ` ? output.replace(/\n/g, `\r
856
+ `) + `\r
857
+ ` : output + `
858
+ `;
859
+ }
860
+ return newline === `\r
861
+ ` ? output.replace(/\n/g, `\r
862
+ `) : output;
722
863
  }
723
864
 
724
865
  // src/index.ts
@@ -0,0 +1,26 @@
1
+ import { type ParserOptions, type SupportOptions } from 'prettier';
2
+ /** import 分组,参考 eslint-plugin-import 的 import/order 规则。 */
3
+ export type ImportGroup = 'builtin' | 'external' | 'internal' | 'parent' | 'sibling' | 'index';
4
+ /** `import type` 内联风格,参考 @typescript-eslint/consistent-type-imports 的 fixStyle 选项。 */
5
+ export type TypeImportsStyle = 'separate' | 'inline-first' | 'inline-last' | 'mixed';
6
+ /** 插件排序配置。 */
7
+ export interface SortOptions {
8
+ importOrder?: boolean;
9
+ importOrderGroups?: ImportGroup[];
10
+ importOrderSeparation?: boolean;
11
+ importOrderTypeImports?: TypeImportsStyle;
12
+ importOrderMergeDuplicates?: boolean;
13
+ exportOrder?: boolean;
14
+ packageJsonOrder?: boolean;
15
+ packageJsonOrderExcludeKeys?: string[];
16
+ }
17
+ /** 默认排序配置。 */
18
+ export declare const DEFAULT_SORT_OPTIONS: Required<SortOptions>;
19
+ type RawSortOptions = ParserOptions & Partial<SortOptions>;
20
+ /**
21
+ * 从 Prettier 选项中提取插件配置,缺失或非法的参数会回退为默认值。
22
+ */
23
+ export declare function resolveSortOptions(rawOptions: RawSortOptions): Required<SortOptions>;
24
+ /** 注册到 Prettier 的选项定义。使用功能名词前缀命名,因为 Prettier API 不支持嵌套的选项对象。 */
25
+ export declare const options: SupportOptions;
26
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `package.json` 顶层字段的排列顺序。
3
+ * 参考社区标准 sort-package-json 的字段列表:https://github.com/keithamus/sort-package-json/blob/main/index.js#L433-L564
4
+ */
5
+ export declare const PACKAGE_JSON_TOP_LEVEL_ORDER: readonly string[];
6
+ /**
7
+ * 值为 `{ name: range }` 映射的依赖表字段。
8
+ * 始终按字母序排列,不受用户选项影响——npm install 每次都会按字母序重写。
9
+ */
10
+ export declare const DEPENDENCY_FIELDS: readonly string[];
@@ -0,0 +1,12 @@
1
+ import { type ParserOptions } from 'prettier';
2
+ /**
3
+ * 按字母序排列 `export { … }` 花括号内的命名导出。
4
+ * 不改变语句位置,不跨语句合并,只针对单条 export 语句的花括号内部。
5
+ *
6
+ * 覆盖的语法形式:
7
+ * export { a, b };
8
+ * export { a, b } from 'mod';
9
+ * export type { A, B };
10
+ * export type { A, B } from 'mod';
11
+ */
12
+ export declare function sortExports(text: string, rawOptions: ParserOptions): string;
@@ -0,0 +1,2 @@
1
+ import { type ParserOptions } from 'prettier';
2
+ export declare function sortImports(text: string, rawOptions: ParserOptions): string;
@@ -0,0 +1,2 @@
1
+ import { type ParserOptions } from 'prettier';
2
+ export declare function sortPackageJson(text: string, rawOptions: ParserOptions): string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 按 `separator` 分割字符串,忽略嵌套在 `{}` / `()` / `[]` 中的分隔符。
3
+ * 仅在顶层(深度为 0 的位置)分隔。返回结果会 trim 并剔除空段。
4
+ *
5
+ * 不使用 `String.prototype.split` 直接分隔,因为 import / export 的花括号内可能
6
+ * 出现形如 `{ a, b as c }`,其中嵌套结构内的逗号不应被当作分隔符。
7
+ */
8
+ export declare function splitTopLevel(input: string, separator: string): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-sort",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An all-in-one Prettier plugin to sort imports, package.json keys, and more.",
5
5
  "keywords": [
6
6
  "imports",
@@ -35,24 +35,25 @@
35
35
  "dist"
36
36
  ],
37
37
  "scripts": {
38
- "build": "bun run scripts/build/index.ts",
39
- "format": "prettier \"{src,scripts}/**/*.ts\" \"*.ts\" --write",
40
- "lint": "eslint \"{src,scripts}/**/*.ts\" \"*.ts\"",
38
+ "build": "bun run scripts/build/",
39
+ "format": "prettier **/*.ts --write",
40
+ "lint": "eslint **/*.ts",
41
41
  "prepare": "husky",
42
- "test": "bun test"
42
+ "test": "bun test scripts/test/",
43
+ "typecheck": "tsc --noEmit"
43
44
  },
44
45
  "devDependencies": {
45
- "@commitlint/cli": "^20.5.0",
46
- "@commitlint/config-conventional": "^20.5.0",
47
- "@commitlint/types": "^20.5.0",
46
+ "@commitlint/cli": "^21.0.1",
47
+ "@commitlint/config-conventional": "^21.0.1",
48
+ "@commitlint/types": "^21.0.1",
48
49
  "@eslint/js": "^10.0.1",
49
- "@types/bun": "^1.3.13",
50
- "bun-plugin-dts": "^0.4.0",
51
- "eslint": "^10.2.1",
50
+ "@types/bun": "^1.3.14",
51
+ "annal": "^0.0.2",
52
+ "eslint": "^10.4.0",
52
53
  "husky": "^9.1.7",
53
54
  "prettier": "^3.8.3",
54
55
  "typescript": "^6.0.3",
55
- "typescript-eslint": "^8.59.0"
56
+ "typescript-eslint": "^8.59.3"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "prettier": ">=3.0.0"