apotheke 0.0.1 → 0.1.1
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/LICENSE +21 -0
- package/README.md +37 -209
- package/SKILL.md +249 -0
- package/dist/cli.js +96 -90
- package/dist/index.d.ts +6 -5
- package/dist/index.js +160 -147
- package/package.json +65 -46
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli.ts
|
|
4
|
-
import path3 from "path";
|
|
5
|
-
import fs from "fs";
|
|
6
4
|
import { execFileSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path3 from "path";
|
|
7
7
|
import fg from "fast-glob";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
|
-
import path from "path";
|
|
11
10
|
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import path from "path";
|
|
12
12
|
function mergeConfigs(parent, child) {
|
|
13
13
|
const mergedGroups = [...parent.groups];
|
|
14
14
|
for (const childGroup of child.groups ?? []) {
|
|
@@ -74,65 +74,6 @@ function loadTsConfig(dir) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// src/parser.ts
|
|
78
|
-
import { parseSync } from "oxc-parser";
|
|
79
|
-
function parseImports(source) {
|
|
80
|
-
const result = parseSync("file.tsx", source);
|
|
81
|
-
const comments = result.comments;
|
|
82
|
-
const nodes = [];
|
|
83
|
-
for (const node of result.program.body) {
|
|
84
|
-
if (node.type !== "ImportDeclaration") continue;
|
|
85
|
-
const specifier = node.source.value;
|
|
86
|
-
let defaultImport;
|
|
87
|
-
const namedImports = [];
|
|
88
|
-
let namespaceImport;
|
|
89
|
-
for (const s of node.specifiers) {
|
|
90
|
-
if (s.type === "ImportDefaultSpecifier") {
|
|
91
|
-
defaultImport = s.local.name;
|
|
92
|
-
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
93
|
-
namespaceImport = s.local.name;
|
|
94
|
-
} else if (s.type === "ImportSpecifier") {
|
|
95
|
-
const imported = s.imported;
|
|
96
|
-
const name = imported.type === "Identifier" ? imported.name : imported.value;
|
|
97
|
-
const localName = s.local.name;
|
|
98
|
-
const kind = s.importKind === "type" ? "type" : "value";
|
|
99
|
-
const named = { name, kind };
|
|
100
|
-
if (localName !== name) named.alias = localName;
|
|
101
|
-
namedImports.push(named);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const importKind = node.importKind === "type" ? "type" : "value";
|
|
105
|
-
const isSideEffect = node.specifiers.length === 0 && importKind === "value";
|
|
106
|
-
const importNode = {
|
|
107
|
-
specifier,
|
|
108
|
-
namedImports,
|
|
109
|
-
isSideEffect,
|
|
110
|
-
importKind,
|
|
111
|
-
start: node.start,
|
|
112
|
-
end: node.end
|
|
113
|
-
};
|
|
114
|
-
if (defaultImport) importNode.defaultImport = defaultImport;
|
|
115
|
-
if (namespaceImport) importNode.namespaceImport = namespaceImport;
|
|
116
|
-
const attached = findAttachedComment(source, node.start, comments);
|
|
117
|
-
if (attached) importNode.attachedComment = attached;
|
|
118
|
-
nodes.push(importNode);
|
|
119
|
-
}
|
|
120
|
-
return nodes;
|
|
121
|
-
}
|
|
122
|
-
function findAttachedComment(source, importStart, comments) {
|
|
123
|
-
const linesBefore = source.slice(0, importStart).split("\n");
|
|
124
|
-
const importLine = linesBefore.length - 1;
|
|
125
|
-
for (const comment of comments) {
|
|
126
|
-
if (comment.type !== "Line") continue;
|
|
127
|
-
const commentLines = source.slice(0, comment.end).split("\n");
|
|
128
|
-
const commentLine = commentLines.length - 1;
|
|
129
|
-
if (commentLine === importLine - 1) {
|
|
130
|
-
return `//${comment.value}`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return void 0;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
77
|
// src/deduplicator.ts
|
|
137
78
|
function deduplicateImports(imports) {
|
|
138
79
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -161,6 +102,8 @@ function deduplicateImports(imports) {
|
|
|
161
102
|
|
|
162
103
|
// src/grouper.ts
|
|
163
104
|
import path2 from "path";
|
|
105
|
+
var SIDE_EFFECTS_GROUP = "SideEffects";
|
|
106
|
+
var OTHERS_GROUP = "Others";
|
|
164
107
|
function groupImports(imports, config, options = {}) {
|
|
165
108
|
const buckets = /* @__PURE__ */ new Map();
|
|
166
109
|
for (const node of imports) {
|
|
@@ -169,21 +112,21 @@ function groupImports(imports, config, options = {}) {
|
|
|
169
112
|
buckets.get(groupName).push(node);
|
|
170
113
|
}
|
|
171
114
|
const result = [];
|
|
172
|
-
if (buckets.has(
|
|
173
|
-
result.push({ name:
|
|
115
|
+
if (buckets.has(SIDE_EFFECTS_GROUP)) {
|
|
116
|
+
result.push({ name: SIDE_EFFECTS_GROUP, imports: buckets.get(SIDE_EFFECTS_GROUP) });
|
|
174
117
|
}
|
|
175
118
|
for (const group of config.groups) {
|
|
176
119
|
if (buckets.has(group.name)) {
|
|
177
120
|
result.push({ name: group.name, imports: buckets.get(group.name) });
|
|
178
121
|
}
|
|
179
122
|
}
|
|
180
|
-
if (buckets.has(
|
|
181
|
-
result.push({ name:
|
|
123
|
+
if (buckets.has(OTHERS_GROUP)) {
|
|
124
|
+
result.push({ name: OTHERS_GROUP, imports: buckets.get(OTHERS_GROUP) });
|
|
182
125
|
}
|
|
183
126
|
return result;
|
|
184
127
|
}
|
|
185
128
|
function assignGroup(node, config, options) {
|
|
186
|
-
if (node.isSideEffect) return
|
|
129
|
+
if (node.isSideEffect) return SIDE_EFFECTS_GROUP;
|
|
187
130
|
const canonicalPath = resolveCanonicalPath(node.specifier, config, options);
|
|
188
131
|
for (const group of config.groups) {
|
|
189
132
|
for (const pattern of group.match) {
|
|
@@ -191,7 +134,7 @@ function assignGroup(node, config, options) {
|
|
|
191
134
|
if (canonicalPath && matches(canonicalPath, pattern)) return group.name;
|
|
192
135
|
}
|
|
193
136
|
}
|
|
194
|
-
return
|
|
137
|
+
return OTHERS_GROUP;
|
|
195
138
|
}
|
|
196
139
|
function resolveCanonicalPath(specifier, config, options) {
|
|
197
140
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
@@ -241,21 +184,63 @@ function globToRegex(pattern) {
|
|
|
241
184
|
return new RegExp(re + "$");
|
|
242
185
|
}
|
|
243
186
|
|
|
244
|
-
// src/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
187
|
+
// src/parser.ts
|
|
188
|
+
import { parseSync } from "oxc-parser";
|
|
189
|
+
function parseImports(source) {
|
|
190
|
+
const result = parseSync("file.tsx", source);
|
|
191
|
+
const comments = result.comments;
|
|
192
|
+
const nodes = [];
|
|
193
|
+
for (const node of result.program.body) {
|
|
194
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
195
|
+
const specifier = node.source.value;
|
|
196
|
+
let defaultImport;
|
|
197
|
+
const namedImports = [];
|
|
198
|
+
let namespaceImport;
|
|
199
|
+
for (const s of node.specifiers) {
|
|
200
|
+
if (s.type === "ImportDefaultSpecifier") {
|
|
201
|
+
defaultImport = s.local.name;
|
|
202
|
+
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
203
|
+
namespaceImport = s.local.name;
|
|
204
|
+
} else if (s.type === "ImportSpecifier") {
|
|
205
|
+
const imported = s.imported;
|
|
206
|
+
const name = imported.type === "Identifier" ? imported.name : imported.value;
|
|
207
|
+
const localName = s.local.name;
|
|
208
|
+
const kind = s.importKind === "type" ? "type" : "value";
|
|
209
|
+
const named = { name, kind };
|
|
210
|
+
if (localName !== name) named.alias = localName;
|
|
211
|
+
namedImports.push(named);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const importKind = node.importKind === "type" ? "type" : "value";
|
|
215
|
+
const isSideEffect = node.specifiers.length === 0 && importKind === "value";
|
|
216
|
+
const importNode = {
|
|
217
|
+
specifier,
|
|
218
|
+
namedImports,
|
|
219
|
+
isSideEffect,
|
|
220
|
+
importKind,
|
|
221
|
+
start: node.start,
|
|
222
|
+
end: node.end
|
|
223
|
+
};
|
|
224
|
+
if (defaultImport) importNode.defaultImport = defaultImport;
|
|
225
|
+
if (namespaceImport) importNode.namespaceImport = namespaceImport;
|
|
226
|
+
const attached = findAttachedComment(source, node.start, comments);
|
|
227
|
+
if (attached) importNode.attachedComment = attached;
|
|
228
|
+
nodes.push(importNode);
|
|
229
|
+
}
|
|
230
|
+
return nodes;
|
|
251
231
|
}
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
232
|
+
function findAttachedComment(source, importStart, comments) {
|
|
233
|
+
const linesBefore = source.slice(0, importStart).split("\n");
|
|
234
|
+
const importLine = linesBefore.length - 1;
|
|
235
|
+
for (const comment of comments) {
|
|
236
|
+
if (comment.type !== "Line") continue;
|
|
237
|
+
const commentLines = source.slice(0, comment.end).split("\n");
|
|
238
|
+
const commentLine = commentLines.length - 1;
|
|
239
|
+
if (commentLine === importLine - 1) {
|
|
240
|
+
return `//${comment.value}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return void 0;
|
|
259
244
|
}
|
|
260
245
|
|
|
261
246
|
// src/printer.ts
|
|
@@ -307,6 +292,23 @@ function buildImportStatement(node, q) {
|
|
|
307
292
|
return `import ${typePrefix}${parts.join(", ")} from ${q}${node.specifier}${q};`;
|
|
308
293
|
}
|
|
309
294
|
|
|
295
|
+
// src/sorter.ts
|
|
296
|
+
function sortGroup(imports) {
|
|
297
|
+
return [...imports].sort((a, b) => {
|
|
298
|
+
if (a.importKind === "type" && b.importKind !== "type") return -1;
|
|
299
|
+
if (a.importKind !== "type" && b.importKind === "type") return 1;
|
|
300
|
+
return a.specifier.localeCompare(b.specifier);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function sortNamedImports(node) {
|
|
304
|
+
const sorted = [...node.namedImports].sort((a, b) => {
|
|
305
|
+
if (a.kind === "type" && b.kind !== "type") return -1;
|
|
306
|
+
if (a.kind !== "type" && b.kind === "type") return 1;
|
|
307
|
+
return a.name.localeCompare(b.name);
|
|
308
|
+
});
|
|
309
|
+
return { ...node, namedImports: sorted };
|
|
310
|
+
}
|
|
311
|
+
|
|
310
312
|
// src/format.ts
|
|
311
313
|
function collectOrphanSegments(source, imports) {
|
|
312
314
|
const orphans = [];
|
|
@@ -335,7 +337,9 @@ function formatImports(source, config, options = {}) {
|
|
|
335
337
|
const deduped = deduplicateImports(clean);
|
|
336
338
|
const sorted = deduped.map(sortNamedImports);
|
|
337
339
|
const grouped = groupImports(sorted, config, options);
|
|
338
|
-
const sortedGroups = grouped.map(
|
|
340
|
+
const sortedGroups = grouped.map(
|
|
341
|
+
(g) => g.name === SIDE_EFFECTS_GROUP ? g : { ...g, imports: sortGroup(g.imports) }
|
|
342
|
+
);
|
|
339
343
|
const q = detectQuoteChar(source);
|
|
340
344
|
const newImportBlock = printGroups(sortedGroups, config, q);
|
|
341
345
|
const firstImport = imports[0];
|
|
@@ -497,19 +501,21 @@ async function expandGlobs(patterns) {
|
|
|
497
501
|
}
|
|
498
502
|
function getSubmoduleDirs(cwd) {
|
|
499
503
|
try {
|
|
500
|
-
const output = execFileSync(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
+
const output = execFileSync(
|
|
505
|
+
"git",
|
|
506
|
+
["-C", cwd, "submodule", "foreach", "--recursive", "--quiet", "pwd"],
|
|
507
|
+
{
|
|
508
|
+
encoding: "utf-8",
|
|
509
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
510
|
+
}
|
|
511
|
+
);
|
|
504
512
|
return output.trim().split("\n").filter(Boolean);
|
|
505
513
|
} catch {
|
|
506
514
|
return [];
|
|
507
515
|
}
|
|
508
516
|
}
|
|
509
517
|
function isUnderSubmodule(filePath, submoduleDirs) {
|
|
510
|
-
return submoduleDirs.some(
|
|
511
|
-
(dir) => filePath === dir || filePath.startsWith(dir + path3.sep)
|
|
512
|
-
);
|
|
518
|
+
return submoduleDirs.some((dir) => filePath === dir || filePath.startsWith(dir + path3.sep));
|
|
513
519
|
}
|
|
514
520
|
function printDiff(file, original, updated) {
|
|
515
521
|
const origLines = original.split("\n");
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ interface ApothekeConfig {
|
|
|
7
7
|
groups: GroupConfig[];
|
|
8
8
|
aliases?: Record<string, string>;
|
|
9
9
|
baseUrl?: string;
|
|
10
|
-
normalizeImports?: 'alias' | 'relative' | 'absolute' | false;
|
|
11
10
|
groupSeparator?: boolean;
|
|
12
11
|
groupComments?: boolean;
|
|
13
12
|
}
|
|
@@ -24,7 +23,7 @@ declare function formatImports(source: string, config: ApothekeConfig, options?:
|
|
|
24
23
|
* Prettier plugin usage (.prettierrc):
|
|
25
24
|
* { "plugins": ["apotheke"] }
|
|
26
25
|
*
|
|
27
|
-
* Requirements: prettier v3+,
|
|
26
|
+
* Requirements: prettier v3+, Node.js >= 18
|
|
28
27
|
*
|
|
29
28
|
* The plugin runs as a `preprocess` step, so apotheke organises imports
|
|
30
29
|
* first and prettier formats the result — correct order, one pass.
|
|
@@ -32,9 +31,11 @@ declare function formatImports(source: string, config: ApothekeConfig, options?:
|
|
|
32
31
|
* Pre-commit (lint-staged, prettier v2):
|
|
33
32
|
* { "*.{ts,tsx}": ["apotheke --write", "prettier --write"] }
|
|
34
33
|
*/
|
|
34
|
+
declare function isDocumentSnippet(parentParser: unknown): boolean;
|
|
35
35
|
type PrettierParser = {
|
|
36
36
|
preprocess?: (text: string, opts: {
|
|
37
37
|
filepath?: string;
|
|
38
|
+
parentParser?: string;
|
|
38
39
|
}) => string | Promise<string>;
|
|
39
40
|
parse?: (text: string, options: unknown) => unknown;
|
|
40
41
|
astFormat?: string;
|
|
@@ -45,8 +46,8 @@ type PrettierParser = {
|
|
|
45
46
|
declare const parsers: {
|
|
46
47
|
typescript: PrettierParser;
|
|
47
48
|
babel: PrettierParser;
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
'babel-ts': PrettierParser;
|
|
50
|
+
'babel-flow': PrettierParser;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
export { type ApothekeConfig, formatImports, parsers };
|
|
53
|
+
export { type ApothekeConfig, formatImports, isDocumentSnippet, parsers };
|