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 +6 -5
- package/README.zh.md +13 -12
- package/dist/index.d.ts +4 -27
- package/dist/index.js +171 -30
- package/dist/options.d.ts +26 -0
- package/dist/order-package.d.ts +10 -0
- package/dist/sort-exports.d.ts +12 -0
- package/dist/sort-imports.d.ts +2 -0
- package/dist/sort-package.d.ts +2 -0
- package/dist/utils.d.ts +8 -0
- package/package.json +13 -12
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
|
|
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:*`, `
|
|
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
|
-
-
|
|
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
|
-
- `
|
|
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`
|
|
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.js
|
|
59
|
+
import 的模块一般可以按其来源分为不同的类别,例如上面的示例代码,'node:fs' 就属于 builtin 分类,该分类涵盖 Node.js 和 Bun **运行时**的内置模块。而像 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:*`、`
|
|
82
|
-
| `external` | npm 包,以及不属于其他分组的模块
|
|
83
|
-
| `internal` | 项目绝对路径与别名
|
|
84
|
-
| `parent` | 向上跳级的相对路径
|
|
85
|
-
| `sibling` | 同级相对路径(不包含 index)
|
|
86
|
-
| `index` | 当前目录的 index 模块
|
|
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
|
-
-
|
|
289
|
+
- `scripts` 和 `betterScripts` 的键名按 pre/post 生命周期包围和 `:` 命名空间分组排序
|
|
290
|
+
- `exports` 键按路径优先、条件字母序、`default` 置底排序,嵌套的 export 对象递归处理
|
|
290
291
|
- `dependencies`、`devDependencies`、`peerDependencies` 等依赖表永远按字母序排列,即使 `packageJsonOrder` 设为 `false` 也不例外。因为 `npm install` 每次都会按照字母序写回
|
|
291
|
-
- `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 === "./" ||
|
|
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+(.+)
|
|
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
|
|
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
|
|
347
|
+
const mergeKey = `${importDecl.source}\x00${importDecl.attributes ?? ""}`;
|
|
348
|
+
const existingIndex = indexByKey.get(mergeKey);
|
|
350
349
|
if (existingIndex === undefined) {
|
|
351
|
-
|
|
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
|
|
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
|
|
378
|
+
const output = [];
|
|
380
379
|
if (typeMembers2.length > 0) {
|
|
381
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
426
|
-
const rewritten =
|
|
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
|
|
508
|
+
const nonBlankIndex = trailing.search(/\S/);
|
|
509
|
+
const suffix = nonBlankIndex >= 0 ? `
|
|
510
510
|
|
|
511
|
-
` + 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
|
-
|
|
650
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
`
|
|
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;
|
package/dist/utils.d.ts
ADDED
|
@@ -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.
|
|
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/
|
|
39
|
-
"format": "prettier
|
|
40
|
-
"lint": "eslint
|
|
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": "^
|
|
46
|
-
"@commitlint/config-conventional": "^
|
|
47
|
-
"@commitlint/types": "^
|
|
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.
|
|
50
|
-
"
|
|
51
|
-
"eslint": "^10.
|
|
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.
|
|
56
|
+
"typescript-eslint": "^8.59.3"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"prettier": ">=3.0.0"
|