apotheke 0.1.0 → 0.1.2
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 +29 -65
- package/dist/cli.js +42 -11
- package/dist/index.js +42 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<p align="left">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/header/graph.svg?title=apotheke&subtitle=Deterministic%20import%20organization%20for%20JavaScript%20and%20TypeScript&logo=https://raw.githubusercontent.com/mRaffaello/apotheke/main/apps/docs/public/mark-dark.png&align=left&font=geist&mode=dark" />
|
|
4
|
+
<img alt="apotheke — deterministic import organization for JavaScript and TypeScript" src="https://shieldcn.dev/header/graph.svg?title=apotheke&subtitle=Deterministic%20import%20organization%20for%20JavaScript%20and%20TypeScript&logo=https://raw.githubusercontent.com/mRaffaello/apotheke/main/apps/docs/public/mark.png&align=left&font=geist&mode=light" />
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="left">
|
|
9
|
+
<a href="https://www.npmjs.com/package/apotheke">
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/group/npm/apotheke+npm/dm/apotheke+bundlephobia/minzip/apotheke+npm/types/apotheke+github/ci/mRaffaello/apotheke+github/license/mRaffaello/apotheke.svg?variant=secondary&mode=dark" />
|
|
12
|
+
<img alt="npm version, monthly downloads, minzipped size, TypeScript types, CI status, MIT license" src="https://shieldcn.dev/group/npm/apotheke+npm/dm/apotheke+bundlephobia/minzip/apotheke+npm/types/apotheke+github/ci/mRaffaello/apotheke+github/license/mRaffaello/apotheke.svg?variant=secondary&mode=light" />
|
|
13
|
+
</picture>
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
**Prettier for imports.** You describe the groups your codebase actually has —
|
|
18
|
+
apotheke sorts every import into them, the same way, in every file. Run it as a
|
|
19
|
+
Prettier plugin or as a standalone CLI.
|
|
7
20
|
|
|
8
21
|
```ts
|
|
9
22
|
// Before
|
|
@@ -28,72 +41,20 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
28
41
|
import { createRoute } from '@tanstack/react-router';
|
|
29
42
|
```
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
Note where `../api/tsr` and `@tanstack/react-query` landed: grouping is by
|
|
45
|
+
**role in your app**, not by whether a module is local or comes from
|
|
46
|
+
`node_modules`.
|
|
34
47
|
|
|
35
|
-
##
|
|
48
|
+
## Getting started
|
|
36
49
|
|
|
37
50
|
```sh
|
|
38
51
|
pnpm add -D apotheke
|
|
39
52
|
```
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Usage
|
|
44
|
-
|
|
45
|
-
Describe the groups you want in `apotheke.config.mjs`:
|
|
46
|
-
|
|
47
|
-
```js
|
|
48
|
-
export default {
|
|
49
|
-
groups: [
|
|
50
|
-
{ name: 'React', match: ['react', 'react-dom'] },
|
|
51
|
-
{ name: 'Hooks', match: ['**/hooks/**'] },
|
|
52
|
-
{ name: 'Api', match: ['**/api/**', '@tanstack/react-query'] },
|
|
53
|
-
{ name: 'Navigation', match: ['@tanstack/react-router'] }
|
|
54
|
-
]
|
|
55
|
-
};
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Then either add the Prettier plugin — apotheke must be **last** in the array:
|
|
59
|
-
|
|
60
|
-
```json
|
|
61
|
-
{ "plugins": ["apotheke"] }
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
or use the CLI:
|
|
65
|
-
|
|
66
|
-
```sh
|
|
67
|
-
apotheke --write 'src/**/*.{ts,tsx}' # rewrite in place
|
|
68
|
-
apotheke --check 'src/**/*.{ts,tsx}' # CI — exit 1 if anything would change
|
|
69
|
-
apotheke --diff 'src/**/*.{ts,tsx}' # preview without writing
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Groups are matched in order and the first match wins. Unmatched imports collect
|
|
73
|
-
in a trailing `Others` group; side-effect imports always come first. Imports are
|
|
74
|
-
sorted and deduplicated within each group, and `tsconfig.json` path aliases are
|
|
75
|
-
read automatically.
|
|
76
|
-
|
|
77
|
-
## Documentation
|
|
78
|
-
|
|
79
|
-
Full guides and reference at
|
|
80
|
-
**[marcoraffaello.com/apotheke](https://marcoraffaello.com/apotheke)**:
|
|
81
|
-
|
|
82
|
-
- [Configuring groups](https://marcoraffaello.com/apotheke/docs/guides/groups)
|
|
83
|
-
- [Aliases and tsconfig paths](https://marcoraffaello.com/apotheke/docs/guides/aliases)
|
|
84
|
-
- [Monorepos](https://marcoraffaello.com/apotheke/docs/guides/monorepos)
|
|
85
|
-
- [Prettier plugin](https://marcoraffaello.com/apotheke/docs/guides/prettier-plugin)
|
|
86
|
-
- [Config reference](https://marcoraffaello.com/apotheke/docs/reference/config)
|
|
54
|
+
📖 **[Read the documentation →](https://marcoraffaello.com/apotheke)**
|
|
87
55
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
This package ships a `SKILL.md` that teaches a coding agent to scan your
|
|
91
|
-
codebase, propose a group config and wire everything up:
|
|
92
|
-
|
|
93
|
-
```sh
|
|
94
|
-
mkdir -p .claude/skills/setup-apotheke
|
|
95
|
-
cp node_modules/apotheke/SKILL.md .claude/skills/setup-apotheke/SKILL.md
|
|
96
|
-
```
|
|
56
|
+
The [quick start](https://marcoraffaello.com/apotheke/docs/quick-start) goes
|
|
57
|
+
from an empty config to organised imports in about two minutes.
|
|
97
58
|
|
|
98
59
|
## Development
|
|
99
60
|
|
|
@@ -107,6 +68,9 @@ pnpm test # unit + e2e
|
|
|
107
68
|
pnpm docs:dev # run the documentation site locally
|
|
108
69
|
```
|
|
109
70
|
|
|
71
|
+
Issues and pull requests are welcome at
|
|
72
|
+
[github.com/mRaffaello/apotheke](https://github.com/mRaffaello/apotheke).
|
|
73
|
+
|
|
110
74
|
## License
|
|
111
75
|
|
|
112
76
|
MIT © Marco Raffaello
|
package/dist/cli.js
CHANGED
|
@@ -102,6 +102,12 @@ function deduplicateImports(imports) {
|
|
|
102
102
|
|
|
103
103
|
// src/grouper.ts
|
|
104
104
|
import path2 from "path";
|
|
105
|
+
|
|
106
|
+
// src/types.ts
|
|
107
|
+
var OTHERS_GROUP = "Others";
|
|
108
|
+
var SIDE_EFFECTS_BLOCK = "SideEffects";
|
|
109
|
+
|
|
110
|
+
// src/grouper.ts
|
|
105
111
|
function groupImports(imports, config, options = {}) {
|
|
106
112
|
const buckets = /* @__PURE__ */ new Map();
|
|
107
113
|
for (const node of imports) {
|
|
@@ -110,21 +116,17 @@ function groupImports(imports, config, options = {}) {
|
|
|
110
116
|
buckets.get(groupName).push(node);
|
|
111
117
|
}
|
|
112
118
|
const result = [];
|
|
113
|
-
if (buckets.has("SideEffects")) {
|
|
114
|
-
result.push({ name: "SideEffects", imports: buckets.get("SideEffects") });
|
|
115
|
-
}
|
|
116
119
|
for (const group of config.groups) {
|
|
117
120
|
if (buckets.has(group.name)) {
|
|
118
121
|
result.push({ name: group.name, imports: buckets.get(group.name) });
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
|
-
if (buckets.has(
|
|
122
|
-
result.push({ name:
|
|
124
|
+
if (buckets.has(OTHERS_GROUP)) {
|
|
125
|
+
result.push({ name: OTHERS_GROUP, imports: buckets.get(OTHERS_GROUP) });
|
|
123
126
|
}
|
|
124
127
|
return result;
|
|
125
128
|
}
|
|
126
129
|
function assignGroup(node, config, options) {
|
|
127
|
-
if (node.isSideEffect) return "SideEffects";
|
|
128
130
|
const canonicalPath = resolveCanonicalPath(node.specifier, config, options);
|
|
129
131
|
for (const group of config.groups) {
|
|
130
132
|
for (const pattern of group.match) {
|
|
@@ -132,7 +134,7 @@ function assignGroup(node, config, options) {
|
|
|
132
134
|
if (canonicalPath && matches(canonicalPath, pattern)) return group.name;
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
|
-
return
|
|
137
|
+
return OTHERS_GROUP;
|
|
136
138
|
}
|
|
137
139
|
function resolveCanonicalPath(specifier, config, options) {
|
|
138
140
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
@@ -255,9 +257,10 @@ function printGroups(groups, config, q = "'") {
|
|
|
255
257
|
const parts = [];
|
|
256
258
|
for (const group of groups) {
|
|
257
259
|
const importLines = [];
|
|
260
|
+
const headed = useComments && group.name !== SIDE_EFFECTS_BLOCK;
|
|
258
261
|
for (let i = 0; i < group.imports.length; i++) {
|
|
259
262
|
const node = group.imports[i];
|
|
260
|
-
if (i === 0 &&
|
|
263
|
+
if (i === 0 && headed) {
|
|
261
264
|
importLines.push(`// ${group.name}
|
|
262
265
|
${buildImportStatement(node, q)}`);
|
|
263
266
|
continue;
|
|
@@ -327,6 +330,35 @@ function collectOrphanSegments(source, imports) {
|
|
|
327
330
|
}
|
|
328
331
|
return orphans;
|
|
329
332
|
}
|
|
333
|
+
function buildBlocks(imports, config, options) {
|
|
334
|
+
const blocks = [];
|
|
335
|
+
let values = [];
|
|
336
|
+
let sideEffects = [];
|
|
337
|
+
function flushValues() {
|
|
338
|
+
if (values.length === 0) return;
|
|
339
|
+
for (const group of groupImports(values, config, options)) {
|
|
340
|
+
blocks.push({ ...group, imports: sortGroup(group.imports) });
|
|
341
|
+
}
|
|
342
|
+
values = [];
|
|
343
|
+
}
|
|
344
|
+
function flushSideEffects() {
|
|
345
|
+
if (sideEffects.length === 0) return;
|
|
346
|
+
blocks.push({ name: SIDE_EFFECTS_BLOCK, imports: sideEffects });
|
|
347
|
+
sideEffects = [];
|
|
348
|
+
}
|
|
349
|
+
for (const node of imports) {
|
|
350
|
+
if (node.isSideEffect) {
|
|
351
|
+
flushValues();
|
|
352
|
+
sideEffects.push(node);
|
|
353
|
+
} else {
|
|
354
|
+
flushSideEffects();
|
|
355
|
+
values.push(node);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
flushSideEffects();
|
|
359
|
+
flushValues();
|
|
360
|
+
return blocks;
|
|
361
|
+
}
|
|
330
362
|
function formatImports(source, config, options = {}) {
|
|
331
363
|
const imports = parseImports(source);
|
|
332
364
|
if (imports.length === 0) return source;
|
|
@@ -334,10 +366,9 @@ function formatImports(source, config, options = {}) {
|
|
|
334
366
|
const clean = imports.map((n) => ({ ...n, attachedComment: void 0 }));
|
|
335
367
|
const deduped = deduplicateImports(clean);
|
|
336
368
|
const sorted = deduped.map(sortNamedImports);
|
|
337
|
-
const
|
|
338
|
-
const sortedGroups = grouped.map((g) => ({ ...g, imports: sortGroup(g.imports) }));
|
|
369
|
+
const blocks = buildBlocks(sorted, config, options);
|
|
339
370
|
const q = detectQuoteChar(source);
|
|
340
|
-
const newImportBlock = printGroups(
|
|
371
|
+
const newImportBlock = printGroups(blocks, config, q);
|
|
341
372
|
const firstImport = imports[0];
|
|
342
373
|
const lastImport = imports[imports.length - 1];
|
|
343
374
|
let regionStart = firstImport.start;
|
package/dist/index.js
CHANGED
|
@@ -99,6 +99,12 @@ function deduplicateImports(imports) {
|
|
|
99
99
|
|
|
100
100
|
// src/grouper.ts
|
|
101
101
|
import path2 from "path";
|
|
102
|
+
|
|
103
|
+
// src/types.ts
|
|
104
|
+
var OTHERS_GROUP = "Others";
|
|
105
|
+
var SIDE_EFFECTS_BLOCK = "SideEffects";
|
|
106
|
+
|
|
107
|
+
// src/grouper.ts
|
|
102
108
|
function groupImports(imports, config, options = {}) {
|
|
103
109
|
const buckets = /* @__PURE__ */ new Map();
|
|
104
110
|
for (const node of imports) {
|
|
@@ -107,21 +113,17 @@ function groupImports(imports, config, options = {}) {
|
|
|
107
113
|
buckets.get(groupName).push(node);
|
|
108
114
|
}
|
|
109
115
|
const result = [];
|
|
110
|
-
if (buckets.has("SideEffects")) {
|
|
111
|
-
result.push({ name: "SideEffects", imports: buckets.get("SideEffects") });
|
|
112
|
-
}
|
|
113
116
|
for (const group of config.groups) {
|
|
114
117
|
if (buckets.has(group.name)) {
|
|
115
118
|
result.push({ name: group.name, imports: buckets.get(group.name) });
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
|
-
if (buckets.has(
|
|
119
|
-
result.push({ name:
|
|
121
|
+
if (buckets.has(OTHERS_GROUP)) {
|
|
122
|
+
result.push({ name: OTHERS_GROUP, imports: buckets.get(OTHERS_GROUP) });
|
|
120
123
|
}
|
|
121
124
|
return result;
|
|
122
125
|
}
|
|
123
126
|
function assignGroup(node, config, options) {
|
|
124
|
-
if (node.isSideEffect) return "SideEffects";
|
|
125
127
|
const canonicalPath = resolveCanonicalPath(node.specifier, config, options);
|
|
126
128
|
for (const group of config.groups) {
|
|
127
129
|
for (const pattern of group.match) {
|
|
@@ -129,7 +131,7 @@ function assignGroup(node, config, options) {
|
|
|
129
131
|
if (canonicalPath && matches(canonicalPath, pattern)) return group.name;
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
|
-
return
|
|
134
|
+
return OTHERS_GROUP;
|
|
133
135
|
}
|
|
134
136
|
function resolveCanonicalPath(specifier, config, options) {
|
|
135
137
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
@@ -252,9 +254,10 @@ function printGroups(groups, config, q = "'") {
|
|
|
252
254
|
const parts = [];
|
|
253
255
|
for (const group of groups) {
|
|
254
256
|
const importLines = [];
|
|
257
|
+
const headed = useComments && group.name !== SIDE_EFFECTS_BLOCK;
|
|
255
258
|
for (let i = 0; i < group.imports.length; i++) {
|
|
256
259
|
const node = group.imports[i];
|
|
257
|
-
if (i === 0 &&
|
|
260
|
+
if (i === 0 && headed) {
|
|
258
261
|
importLines.push(`// ${group.name}
|
|
259
262
|
${buildImportStatement(node, q)}`);
|
|
260
263
|
continue;
|
|
@@ -324,6 +327,35 @@ function collectOrphanSegments(source, imports) {
|
|
|
324
327
|
}
|
|
325
328
|
return orphans;
|
|
326
329
|
}
|
|
330
|
+
function buildBlocks(imports, config, options) {
|
|
331
|
+
const blocks = [];
|
|
332
|
+
let values = [];
|
|
333
|
+
let sideEffects = [];
|
|
334
|
+
function flushValues() {
|
|
335
|
+
if (values.length === 0) return;
|
|
336
|
+
for (const group of groupImports(values, config, options)) {
|
|
337
|
+
blocks.push({ ...group, imports: sortGroup(group.imports) });
|
|
338
|
+
}
|
|
339
|
+
values = [];
|
|
340
|
+
}
|
|
341
|
+
function flushSideEffects() {
|
|
342
|
+
if (sideEffects.length === 0) return;
|
|
343
|
+
blocks.push({ name: SIDE_EFFECTS_BLOCK, imports: sideEffects });
|
|
344
|
+
sideEffects = [];
|
|
345
|
+
}
|
|
346
|
+
for (const node of imports) {
|
|
347
|
+
if (node.isSideEffect) {
|
|
348
|
+
flushValues();
|
|
349
|
+
sideEffects.push(node);
|
|
350
|
+
} else {
|
|
351
|
+
flushSideEffects();
|
|
352
|
+
values.push(node);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
flushSideEffects();
|
|
356
|
+
flushValues();
|
|
357
|
+
return blocks;
|
|
358
|
+
}
|
|
327
359
|
function formatImports(source, config, options = {}) {
|
|
328
360
|
const imports = parseImports(source);
|
|
329
361
|
if (imports.length === 0) return source;
|
|
@@ -331,10 +363,9 @@ function formatImports(source, config, options = {}) {
|
|
|
331
363
|
const clean = imports.map((n) => ({ ...n, attachedComment: void 0 }));
|
|
332
364
|
const deduped = deduplicateImports(clean);
|
|
333
365
|
const sorted = deduped.map(sortNamedImports);
|
|
334
|
-
const
|
|
335
|
-
const sortedGroups = grouped.map((g) => ({ ...g, imports: sortGroup(g.imports) }));
|
|
366
|
+
const blocks = buildBlocks(sorted, config, options);
|
|
336
367
|
const q = detectQuoteChar(source);
|
|
337
|
-
const newImportBlock = printGroups(
|
|
368
|
+
const newImportBlock = printGroups(blocks, config, q);
|
|
338
369
|
const firstImport = imports[0];
|
|
339
370
|
const lastImport = imports[imports.length - 1];
|
|
340
371
|
let regionStart = firstImport.start;
|