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 CHANGED
@@ -1,9 +1,22 @@
1
- # apotheke
2
-
3
- **Prettier for imports.** Deterministic, configurable import organization for
4
- JavaScript and TypeScript as a Prettier plugin or a standalone CLI.
5
-
6
- 📖 **[Documentation](https://marcoraffaello.com/apotheke)**
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="apothekedeterministic 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
- apotheke is not a replacement for Prettier it runs _inside_ Prettier as a
32
- `preprocess` plugin, so one `prettier --write` organises imports and formats
33
- code in a single pass.
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
- ## Installation
48
+ ## Getting started
36
49
 
37
50
  ```sh
38
51
  pnpm add -D apotheke
39
52
  ```
40
53
 
41
- Requires Node.js 18, and Prettier ≥ 3 if you use the plugin.
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
- ## Agent skill
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("Others")) {
122
- result.push({ name: "Others", imports: buckets.get("Others") });
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 "Others";
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 && useComments) {
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 grouped = groupImports(sorted, config, options);
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(sortedGroups, config, q);
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("Others")) {
119
- result.push({ name: "Others", imports: buckets.get("Others") });
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 "Others";
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 && useComments) {
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 grouped = groupImports(sorted, config, options);
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(sortedGroups, config, q);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apotheke",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Prettier for imports — deterministic import organization as a Prettier plugin or CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",