apotheke 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +44 -180
- package/SKILL.md +249 -0
- package/dist/cli.js +85 -83
- package/dist/index.d.ts +6 -5
- package/dist/index.js +149 -140
- package/package.json +65 -46
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marco Raffaello
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# apotheke
|
|
2
2
|
|
|
3
|
-
|
|
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)**
|
|
4
7
|
|
|
5
8
|
```ts
|
|
6
9
|
// Before
|
|
@@ -25,224 +28,85 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
25
28
|
import { createRoute } from '@tanstack/react-router';
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
31
34
|
|
|
32
35
|
## Installation
|
|
33
36
|
|
|
34
37
|
```sh
|
|
35
|
-
npm install -D apotheke
|
|
36
|
-
# or
|
|
37
38
|
pnpm add -D apotheke
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
Requires Node.js ≥ 18, and Prettier ≥ 3 if you use the plugin.
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
apotheke --write src/**/*.{ts,tsx} # format in place
|
|
44
|
-
apotheke --check src/**/*.{ts,tsx} # CI — exit 1 if anything would change
|
|
45
|
-
apotheke --diff src/**/*.{ts,tsx} # print diff without writing
|
|
46
|
-
apotheke --stdin-filepath src/app.tsx # read from stdin, write to stdout
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Config
|
|
43
|
+
## Usage
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
Describe the groups you want in `apotheke.config.mjs`:
|
|
52
46
|
|
|
53
47
|
```js
|
|
54
|
-
// apotheke.config.mjs
|
|
55
48
|
export default {
|
|
56
49
|
groups: [
|
|
57
|
-
{ name: 'React', match: ['react', 'react-dom'
|
|
50
|
+
{ name: 'React', match: ['react', 'react-dom'] },
|
|
58
51
|
{ name: 'Hooks', match: ['**/hooks/**'] },
|
|
59
52
|
{ name: 'Api', match: ['**/api/**', '@tanstack/react-query'] },
|
|
60
|
-
{ name: 'Navigation', match: ['@tanstack/react-router'] }
|
|
61
|
-
|
|
62
|
-
],
|
|
63
|
-
aliases: {
|
|
64
|
-
'@': './src' // mirrors tsconfig paths — auto-read if omitted
|
|
65
|
-
},
|
|
66
|
-
groupSeparator: true, // blank line between groups
|
|
67
|
-
groupComments: true // // GroupName header above each group
|
|
68
|
-
};
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Apotheke also reads `tsconfig.json` (or `jsconfig.json`) automatically to pick up `paths` aliases and `baseUrl`, so you often don't need to set `aliases` manually.
|
|
72
|
-
|
|
73
|
-
Unmatched imports collect in an **Others** group at the end. Side-effect imports (`import './styles.css'`) always go first.
|
|
74
|
-
|
|
75
|
-
### Monorepo
|
|
76
|
-
|
|
77
|
-
Place a root config and extend it per-package:
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
// apps/web/apotheke.config.mjs
|
|
81
|
-
export default {
|
|
82
|
-
extends: '../../apotheke.config.mjs',
|
|
83
|
-
groups: [{ name: 'Shared', match: ['@acme/*'] }]
|
|
84
|
-
};
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## Prettier plugin
|
|
90
|
-
|
|
91
|
-
Apotheke ships as a prettier plugin. When loaded, it runs as a `preprocess` step — apotheke organises imports first, then prettier formats the result. One pass, correct order, no conflicts.
|
|
92
|
-
|
|
93
|
-
**Requirements:** Prettier v3 or later (v3 supports async `preprocess`; v2 does not).
|
|
94
|
-
|
|
95
|
-
### Permanent setup
|
|
96
|
-
|
|
97
|
-
**1. Install apotheke**
|
|
98
|
-
|
|
99
|
-
```sh
|
|
100
|
-
pnpm add -D apotheke
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**2. Add the plugin to your prettier config**
|
|
104
|
-
|
|
105
|
-
```js
|
|
106
|
-
// .prettierrc.js
|
|
107
|
-
module.exports = {
|
|
108
|
-
plugins: ['apotheke'],
|
|
53
|
+
{ name: 'Navigation', match: ['@tanstack/react-router'] }
|
|
54
|
+
]
|
|
109
55
|
};
|
|
110
56
|
```
|
|
111
57
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
### Quick test (without installing)
|
|
115
|
-
|
|
116
|
-
```sh
|
|
117
|
-
cd /path/to/your-project
|
|
118
|
-
npx prettier@3 \
|
|
119
|
-
--plugin /path/to/apotheke/dist/index.js \
|
|
120
|
-
--write 'src/App.tsx'
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### VS Code on-save
|
|
124
|
-
|
|
125
|
-
Install the [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) extension. Because `prettier.format()` calls our plugin's `preprocess` hook directly, no extra config is needed:
|
|
58
|
+
Then either add the Prettier plugin — apotheke must be **last** in the array:
|
|
126
59
|
|
|
127
60
|
```json
|
|
128
|
-
{
|
|
129
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
130
|
-
"editor.formatOnSave": true
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Pre-commit with Husky + lint-staged
|
|
135
|
-
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"lint-staged": {
|
|
139
|
-
"*.{ts,tsx,js,jsx}": ["prettier --write"]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Prettier v2 fallback
|
|
145
|
-
|
|
146
|
-
If you can't upgrade to prettier v3, use the CLI sequentially via lint-staged:
|
|
147
|
-
|
|
148
|
-
```json
|
|
149
|
-
{
|
|
150
|
-
"lint-staged": {
|
|
151
|
-
"*.{ts,tsx,js,jsx}": [
|
|
152
|
-
"apotheke --write",
|
|
153
|
-
"prettier --write"
|
|
154
|
-
]
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Testing locally without publishing
|
|
162
|
-
|
|
163
|
-
### Option 1 — Direct invocation (no setup)
|
|
164
|
-
|
|
165
|
-
```sh
|
|
166
|
-
node /path/to/apotheke/dist/cli.js --write 'src/**/*.{ts,tsx}'
|
|
61
|
+
{ "plugins": ["apotheke"] }
|
|
167
62
|
```
|
|
168
63
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
### Option 2 — `pnpm link` (recommended)
|
|
172
|
-
|
|
173
|
-
**In the apotheke repo:**
|
|
64
|
+
or use the CLI:
|
|
174
65
|
|
|
175
66
|
```sh
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
178
70
|
```
|
|
179
71
|
|
|
180
|
-
|
|
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.
|
|
181
76
|
|
|
182
|
-
|
|
183
|
-
pnpm link --global apotheke
|
|
184
|
-
```
|
|
77
|
+
## Documentation
|
|
185
78
|
|
|
186
|
-
|
|
79
|
+
Full guides and reference at
|
|
80
|
+
**[marcoraffaello.com/apotheke](https://marcoraffaello.com/apotheke)**:
|
|
187
81
|
|
|
188
|
-
|
|
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)
|
|
189
87
|
|
|
190
|
-
|
|
191
|
-
# In your target repo
|
|
192
|
-
pnpm unlink --global apotheke
|
|
193
|
-
|
|
194
|
-
# In the apotheke repo
|
|
195
|
-
pnpm unlink --global
|
|
196
|
-
```
|
|
88
|
+
## Agent skill
|
|
197
89
|
|
|
198
|
-
|
|
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:
|
|
199
92
|
|
|
200
93
|
```sh
|
|
201
|
-
|
|
94
|
+
mkdir -p .claude/skills/setup-apotheke
|
|
95
|
+
cp node_modules/apotheke/SKILL.md .claude/skills/setup-apotheke/SKILL.md
|
|
202
96
|
```
|
|
203
97
|
|
|
204
|
-
---
|
|
205
|
-
|
|
206
98
|
## Development
|
|
207
99
|
|
|
100
|
+
This repository is a pnpm workspace. The published package lives in
|
|
101
|
+
`packages/apotheke`, the documentation site in `apps/docs`.
|
|
102
|
+
|
|
208
103
|
```sh
|
|
209
104
|
pnpm install
|
|
210
|
-
|
|
211
|
-
#
|
|
212
|
-
pnpm
|
|
213
|
-
|
|
214
|
-
# Run all tests (unit + e2e)
|
|
215
|
-
pnpm test
|
|
216
|
-
|
|
217
|
-
# Unit tests only
|
|
218
|
-
pnpm test:unit
|
|
219
|
-
|
|
220
|
-
# E2E tests against real repos (cloned automatically on first run)
|
|
221
|
-
pnpm test:e2e
|
|
222
|
-
|
|
223
|
-
# Type check
|
|
224
|
-
pnpm typecheck
|
|
105
|
+
pnpm build # build the package (required before format and dogfooding)
|
|
106
|
+
pnpm test # unit + e2e
|
|
107
|
+
pnpm docs:dev # run the documentation site locally
|
|
225
108
|
```
|
|
226
109
|
|
|
227
|
-
|
|
110
|
+
## License
|
|
228
111
|
|
|
229
|
-
|
|
230
|
-
src/
|
|
231
|
-
parser.ts OXC-based import extractor
|
|
232
|
-
grouper.ts Glob-based group assignment
|
|
233
|
-
sorter.ts Alphabetical sort, type imports float to top
|
|
234
|
-
deduplicator.ts Merge named/default imports from same specifier
|
|
235
|
-
printer.ts Reconstruct import block with group headers
|
|
236
|
-
config.ts Load apotheke.config.mjs, merge tsconfig aliases
|
|
237
|
-
format.ts Top-level formatImports(source, config)
|
|
238
|
-
types.ts Shared types
|
|
239
|
-
cli.ts CLI source (Node.js)
|
|
240
|
-
index.ts Prettier plugin + programmatic API
|
|
241
|
-
dist/
|
|
242
|
-
cli.js Compiled CLI — run with node or via the apotheke bin
|
|
243
|
-
index.js Compiled prettier plugin
|
|
244
|
-
index.d.ts Types for programmatic use
|
|
245
|
-
tests/
|
|
246
|
-
unit/ 76 unit tests
|
|
247
|
-
e2e/ 20 e2e tests against sonner and tremor
|
|
248
|
-
```
|
|
112
|
+
MIT © Marco Raffaello
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-apotheke
|
|
3
|
+
description: Set up apotheke import organizer in any JS/TS project. Scans existing imports, proposes group config, writes apotheke.config.mjs, and wires up prettier plugin. Use when user wants to organize imports, set up apotheke, or add import formatting to a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Setup Apotheke
|
|
7
|
+
|
|
8
|
+
Apotheke is **Prettier for imports** — deterministic, configurable import organization for JavaScript and TypeScript. It is not a replacement for prettier: it runs *inside* prettier as a `preprocess` plugin, so imports get organized and code gets formatted in a single `prettier --write` pass. It also ships a standalone CLI for projects that do not use prettier.
|
|
9
|
+
|
|
10
|
+
This skill installs and configures it by analyzing the project's actual imports, then wiring it up as a prettier plugin.
|
|
11
|
+
|
|
12
|
+
Full documentation: <https://marcoraffaello.com/apotheke>
|
|
13
|
+
|
|
14
|
+
## Step 1 — Detect project structure (monorepo vs single package)
|
|
15
|
+
|
|
16
|
+
Before doing anything else, determine whether this is a monorepo. Check for these signals at the working directory root:
|
|
17
|
+
|
|
18
|
+
| File / field | Tool |
|
|
19
|
+
| ---------------------------------------- | ----------------------- |
|
|
20
|
+
| `pnpm-workspace.yaml` | pnpm workspaces |
|
|
21
|
+
| `turbo.json` | Turborepo |
|
|
22
|
+
| `nx.json` | Nx |
|
|
23
|
+
| `lerna.json` | Lerna |
|
|
24
|
+
| `rush.json` | Rush |
|
|
25
|
+
| `package.json` with `"workspaces"` field | npm/yarn/bun workspaces |
|
|
26
|
+
|
|
27
|
+
If any of these are present, it's a **monorepo**. Follow the monorepo path below. Otherwise follow the single-package path.
|
|
28
|
+
|
|
29
|
+
### Single-package path
|
|
30
|
+
|
|
31
|
+
Proceed to Step 2 with the repo root as the single package root.
|
|
32
|
+
|
|
33
|
+
### Monorepo path
|
|
34
|
+
|
|
35
|
+
1. Identify all package roots by reading the workspaces glob (e.g. `packages/*`, `apps/*`) from whichever workspace config is present. List them to the user.
|
|
36
|
+
2. Ask: "Should I set up a shared root config that all packages inherit from, or configure each package independently?"
|
|
37
|
+
- **Shared root** (recommended): write a root `apotheke.config.mjs` with groups common across all packages; each package gets its own config with `extends: "../../apotheke.config.mjs"` that overrides or adds package-specific groups.
|
|
38
|
+
- **Independent**: each package gets its own self-contained config with no inheritance.
|
|
39
|
+
3. Scan each package separately (Step 2 runs once per package root). Combine the findings to build the shared group set before writing configs.
|
|
40
|
+
4. In Step 6, add scripts to each package's `package.json` scoped to that package's source folder. Optionally add a root-level script that runs prettier across all packages.
|
|
41
|
+
|
|
42
|
+
## Step 2 — Scan the project
|
|
43
|
+
|
|
44
|
+
For each package root (or just the single root), search for all `.ts`, `.tsx`, `.js`, `.jsx` files (excluding `node_modules`, `dist`, `.next`, `build`, `out`). Read a sample of files (up to 20 per package, spread across the directory tree) and extract the unique import specifiers.
|
|
45
|
+
|
|
46
|
+
Pay attention to:
|
|
47
|
+
|
|
48
|
+
- **Package imports**: `react`, `@tanstack/react-query`, `lucide-react`, etc.
|
|
49
|
+
- **Path alias imports**: `@/components/...`, `~/lib/...`, `#utils/...`
|
|
50
|
+
- **Relative path patterns**: which folders appear most — `hooks/`, `api/`, `components/`, `views/`, `pages/`, `utils/`, `lib/`, `stores/`, etc.
|
|
51
|
+
- **tsconfig.json / jsconfig.json**: read `compilerOptions.paths` and `compilerOptions.baseUrl` to understand existing aliases. In a monorepo, check both the root tsconfig and each package's tsconfig.
|
|
52
|
+
|
|
53
|
+
## Step 3 — Check for existing config
|
|
54
|
+
|
|
55
|
+
Look for `apotheke.config.mjs`, `apotheke.config.js`, or `apotheke.config.ts` at each relevant root. If one already exists, read it, tell the user, and ask whether to update it or abort.
|
|
56
|
+
|
|
57
|
+
## Step 4 — Propose groups
|
|
58
|
+
|
|
59
|
+
Based on what you found, propose a `groups` array. Use your judgment to create meaningful groups. Common patterns:
|
|
60
|
+
|
|
61
|
+
- If the project uses React → group `react`, `react-dom`, `react-*`
|
|
62
|
+
> **Warning — `react-*` is greedy**: it will also match `react-hook-form`, `react-i18next`, `react-day-picker`, etc. Groups are matched in order and the first match wins. If you want those libraries in their own groups (Forms, i18n, Date), either list them in those groups _before_ the React group in the array, or narrow the React match to just `['react', 'react-dom']` without the wildcard.
|
|
63
|
+
- If the project has `hooks/` folder → group `**/hooks/**`
|
|
64
|
+
- If the project has `api/`, `services/`, `queries/` → group those paths + data-fetching libraries (react-query, swr, etc.)
|
|
65
|
+
- If the project has `components/`, `ui/` → group those
|
|
66
|
+
- If the project uses a router (react-router, tanstack-router, next/navigation) → group it
|
|
67
|
+
- If the project uses icon libraries (lucide-react, @heroicons, react-icons) → group as Assets
|
|
68
|
+
- If the project has a `store/`, `context/` folder → group it
|
|
69
|
+
- If the project uses a UI kit (@mui, @radix-ui, shadcn) → group it
|
|
70
|
+
- In a monorepo, internal workspace packages (e.g. `@acme/*`) deserve their own group
|
|
71
|
+
|
|
72
|
+
Always propose a sensible **alias map** based on tsconfig `paths`.
|
|
73
|
+
|
|
74
|
+
In a monorepo with a shared root config: put universal groups (React, external libraries) in the root config, and package-specific groups (internal paths, local components) in each package config.
|
|
75
|
+
|
|
76
|
+
Present the proposed config clearly and ask the user:
|
|
77
|
+
|
|
78
|
+
1. Are these groups right? Any to add, remove, or rename?
|
|
79
|
+
2. Is the order right? Groups are matched top-to-bottom and the first match wins.
|
|
80
|
+
3. Should there be blank lines between groups? (`groupSeparator`, default: yes)
|
|
81
|
+
4. Should group comments (`// React`) be added? (`groupComments`, default: yes)
|
|
82
|
+
|
|
83
|
+
Do **not** offer to toggle sorting or deduplication. Imports are always sorted
|
|
84
|
+
alphabetically within a group, named imports are always sorted within the
|
|
85
|
+
braces, and duplicate specifiers are always merged. There are no options for
|
|
86
|
+
these.
|
|
87
|
+
|
|
88
|
+
Wait for confirmation before proceeding.
|
|
89
|
+
|
|
90
|
+
## Step 5 — Write apotheke.config.mjs
|
|
91
|
+
|
|
92
|
+
**Always use `.mjs`** — it works with both the CLI and the Node.js prettier plugin. Never write `.ts` configs: Node.js (where prettier runs) cannot `import()` TypeScript files. Only `apotheke.config.mjs` and `apotheke.config.js` are discovered.
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// apotheke.config.mjs
|
|
96
|
+
export default {
|
|
97
|
+
groups: [
|
|
98
|
+
// ... confirmed groups
|
|
99
|
+
],
|
|
100
|
+
aliases: {
|
|
101
|
+
// ... from tsconfig paths
|
|
102
|
+
},
|
|
103
|
+
groupSeparator: true,
|
|
104
|
+
groupComments: true
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For a monorepo with a shared root:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// apps/web/apotheke.config.mjs
|
|
112
|
+
export default {
|
|
113
|
+
extends: '../../apotheke.config.mjs',
|
|
114
|
+
groups: [
|
|
115
|
+
// package-specific groups only — root groups are inherited
|
|
116
|
+
{ name: 'Local Components', match: ['**/components/**'] }
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Step 6 — Install apotheke and wire up prettier
|
|
122
|
+
|
|
123
|
+
### 6a. Install apotheke
|
|
124
|
+
|
|
125
|
+
Check if apotheke is already in `package.json` dependencies. If not, install it:
|
|
126
|
+
|
|
127
|
+
- Bun project (`bun.lock` present) → `bun add -D apotheke`
|
|
128
|
+
- pnpm (`pnpm-lock.yaml`) → `pnpm add -D apotheke`
|
|
129
|
+
- yarn (`yarn.lock`) → `yarn add -D apotheke`
|
|
130
|
+
- npm (`package-lock.json`) → `npm install -D apotheke`
|
|
131
|
+
|
|
132
|
+
In a monorepo, install at the root unless the workspace requires per-package installs.
|
|
133
|
+
|
|
134
|
+
### 6b. Add apotheke to the prettier config
|
|
135
|
+
|
|
136
|
+
Check for `.prettierrc`, `.prettierrc.js`, `.prettierrc.json`, or `prettier.config.js`. Add `"apotheke"` to the `plugins` array (create the file if none exists):
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
// prettier.config.js (or .prettierrc.js)
|
|
140
|
+
module.exports = {
|
|
141
|
+
// ...existing options unchanged...
|
|
142
|
+
plugins: ['apotheke']
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For JSON prettier configs (`.prettierrc` or `.prettierrc.json`):
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"plugins": ["apotheke"]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
> **Ordering is critical**: apotheke must be the **last** entry in `plugins`. It chains to the previous plugin's `preprocess` hook via `getBase()?.preprocess`, so any plugin listed after it will completely override apotheke's preprocess and imports will not be organized. If another prettier plugin (e.g. `prettier-plugin-tailwindcss`) is already present, place `"apotheke"` after it:
|
|
155
|
+
>
|
|
156
|
+
> ```json
|
|
157
|
+
> { "plugins": ["prettier-plugin-tailwindcss", "apotheke"] }
|
|
158
|
+
> ```
|
|
159
|
+
|
|
160
|
+
### 6c. Verify prettier version
|
|
161
|
+
|
|
162
|
+
Check that prettier v3 is installed (`"prettier": "^3"` in devDependencies). If v2 is found, tell the user:
|
|
163
|
+
|
|
164
|
+
> "Apotheke's prettier plugin requires prettier v3 (async `preprocess`). I can upgrade it, or set up the v2 fallback (CLI + prettier in sequence). Which do you prefer?"
|
|
165
|
+
|
|
166
|
+
**v2 fallback** (lint-staged only):
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"lint-staged": {
|
|
171
|
+
"*.{ts,tsx,js,jsx}": ["apotheke --write", "prettier --write"]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Step 7 — Add scripts to package.json
|
|
177
|
+
|
|
178
|
+
With the prettier plugin wired up, a single `prettier --write` does everything. Add:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"scripts": {
|
|
183
|
+
"format": "prettier --write 'src/**/*.{ts,tsx,js,jsx}'",
|
|
184
|
+
"format:check": "prettier --check 'src/**/*.{ts,tsx,js,jsx}'"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Adjust the glob to match the project's source folder. In a monorepo, also add a root script:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"scripts": {
|
|
194
|
+
"format": "prettier --write 'packages/*/src/**/*.{ts,tsx}' 'apps/*/src/**/*.{ts,tsx}'",
|
|
195
|
+
"format:check": "prettier --check 'packages/*/src/**/*.{ts,tsx}' 'apps/*/src/**/*.{ts,tsx}'"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Step 8 — VS Code on-save (optional)
|
|
201
|
+
|
|
202
|
+
Ask the user: "Do you want imports organized automatically on save in VS Code?"
|
|
203
|
+
|
|
204
|
+
If yes, the prettier VS Code extension handles this automatically once the plugin is in the prettier config — no extra config needed. Confirm that the extension is installed and `editor.formatOnSave` is enabled:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
// .vscode/settings.json
|
|
208
|
+
{
|
|
209
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
210
|
+
"editor.formatOnSave": true,
|
|
211
|
+
"editor.codeActionsOnSave": {
|
|
212
|
+
"source.organizeImports": false
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Disable `source.organizeImports` to prevent VS Code's built-in import sorter from conflicting with apotheke.
|
|
218
|
+
|
|
219
|
+
## Step 9 — Run a dry-run
|
|
220
|
+
|
|
221
|
+
Run a true dry-run (no writes) on a small sample (3–5 files, one per package in a monorepo) and show the user what would change:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
prettier --list-different 'src/App.tsx' 'src/main.tsx'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> `--write --list-different` together would actually write the files; use `--list-different` alone for a real dry-run.
|
|
228
|
+
|
|
229
|
+
Show the before/after for imports to confirm the groups look right.
|
|
230
|
+
|
|
231
|
+
Ask: "Does this look right? Shall I run `format` on the whole project?"
|
|
232
|
+
|
|
233
|
+
If yes, run the `format` script using the package manager detected in Step 6a
|
|
234
|
+
(`pnpm run format`, `bun run format`, `yarn format` or `npm run format`).
|
|
235
|
+
|
|
236
|
+
## Final summary
|
|
237
|
+
|
|
238
|
+
Report:
|
|
239
|
+
|
|
240
|
+
- Project type: single-package or monorepo (list package roots if monorepo)
|
|
241
|
+
- Config(s) written to: paths
|
|
242
|
+
- Groups configured: list them
|
|
243
|
+
- Prettier plugin: wired up in `prettier.config.js` (or equivalent)
|
|
244
|
+
- Script added: `format` / `format:check`
|
|
245
|
+
- Files that would be changed: N files
|
|
246
|
+
- Whether `--write` was run
|
|
247
|
+
|
|
248
|
+
Point the user at the documentation for anything beyond this setup:
|
|
249
|
+
<https://marcoraffaello.com/apotheke>
|
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();
|
|
@@ -241,21 +182,63 @@ function globToRegex(pattern) {
|
|
|
241
182
|
return new RegExp(re + "$");
|
|
242
183
|
}
|
|
243
184
|
|
|
244
|
-
// src/
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
185
|
+
// src/parser.ts
|
|
186
|
+
import { parseSync } from "oxc-parser";
|
|
187
|
+
function parseImports(source) {
|
|
188
|
+
const result = parseSync("file.tsx", source);
|
|
189
|
+
const comments = result.comments;
|
|
190
|
+
const nodes = [];
|
|
191
|
+
for (const node of result.program.body) {
|
|
192
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
193
|
+
const specifier = node.source.value;
|
|
194
|
+
let defaultImport;
|
|
195
|
+
const namedImports = [];
|
|
196
|
+
let namespaceImport;
|
|
197
|
+
for (const s of node.specifiers) {
|
|
198
|
+
if (s.type === "ImportDefaultSpecifier") {
|
|
199
|
+
defaultImport = s.local.name;
|
|
200
|
+
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
201
|
+
namespaceImport = s.local.name;
|
|
202
|
+
} else if (s.type === "ImportSpecifier") {
|
|
203
|
+
const imported = s.imported;
|
|
204
|
+
const name = imported.type === "Identifier" ? imported.name : imported.value;
|
|
205
|
+
const localName = s.local.name;
|
|
206
|
+
const kind = s.importKind === "type" ? "type" : "value";
|
|
207
|
+
const named = { name, kind };
|
|
208
|
+
if (localName !== name) named.alias = localName;
|
|
209
|
+
namedImports.push(named);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const importKind = node.importKind === "type" ? "type" : "value";
|
|
213
|
+
const isSideEffect = node.specifiers.length === 0 && importKind === "value";
|
|
214
|
+
const importNode = {
|
|
215
|
+
specifier,
|
|
216
|
+
namedImports,
|
|
217
|
+
isSideEffect,
|
|
218
|
+
importKind,
|
|
219
|
+
start: node.start,
|
|
220
|
+
end: node.end
|
|
221
|
+
};
|
|
222
|
+
if (defaultImport) importNode.defaultImport = defaultImport;
|
|
223
|
+
if (namespaceImport) importNode.namespaceImport = namespaceImport;
|
|
224
|
+
const attached = findAttachedComment(source, node.start, comments);
|
|
225
|
+
if (attached) importNode.attachedComment = attached;
|
|
226
|
+
nodes.push(importNode);
|
|
227
|
+
}
|
|
228
|
+
return nodes;
|
|
251
229
|
}
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
230
|
+
function findAttachedComment(source, importStart, comments) {
|
|
231
|
+
const linesBefore = source.slice(0, importStart).split("\n");
|
|
232
|
+
const importLine = linesBefore.length - 1;
|
|
233
|
+
for (const comment of comments) {
|
|
234
|
+
if (comment.type !== "Line") continue;
|
|
235
|
+
const commentLines = source.slice(0, comment.end).split("\n");
|
|
236
|
+
const commentLine = commentLines.length - 1;
|
|
237
|
+
if (commentLine === importLine - 1) {
|
|
238
|
+
return `//${comment.value}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
259
242
|
}
|
|
260
243
|
|
|
261
244
|
// src/printer.ts
|
|
@@ -307,6 +290,23 @@ function buildImportStatement(node, q) {
|
|
|
307
290
|
return `import ${typePrefix}${parts.join(", ")} from ${q}${node.specifier}${q};`;
|
|
308
291
|
}
|
|
309
292
|
|
|
293
|
+
// src/sorter.ts
|
|
294
|
+
function sortGroup(imports) {
|
|
295
|
+
return [...imports].sort((a, b) => {
|
|
296
|
+
if (a.importKind === "type" && b.importKind !== "type") return -1;
|
|
297
|
+
if (a.importKind !== "type" && b.importKind === "type") return 1;
|
|
298
|
+
return a.specifier.localeCompare(b.specifier);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function sortNamedImports(node) {
|
|
302
|
+
const sorted = [...node.namedImports].sort((a, b) => {
|
|
303
|
+
if (a.kind === "type" && b.kind !== "type") return -1;
|
|
304
|
+
if (a.kind !== "type" && b.kind === "type") return 1;
|
|
305
|
+
return a.name.localeCompare(b.name);
|
|
306
|
+
});
|
|
307
|
+
return { ...node, namedImports: sorted };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
310
|
// src/format.ts
|
|
311
311
|
function collectOrphanSegments(source, imports) {
|
|
312
312
|
const orphans = [];
|
|
@@ -497,19 +497,21 @@ async function expandGlobs(patterns) {
|
|
|
497
497
|
}
|
|
498
498
|
function getSubmoduleDirs(cwd) {
|
|
499
499
|
try {
|
|
500
|
-
const output = execFileSync(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
500
|
+
const output = execFileSync(
|
|
501
|
+
"git",
|
|
502
|
+
["-C", cwd, "submodule", "foreach", "--recursive", "--quiet", "pwd"],
|
|
503
|
+
{
|
|
504
|
+
encoding: "utf-8",
|
|
505
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
506
|
+
}
|
|
507
|
+
);
|
|
504
508
|
return output.trim().split("\n").filter(Boolean);
|
|
505
509
|
} catch {
|
|
506
510
|
return [];
|
|
507
511
|
}
|
|
508
512
|
}
|
|
509
513
|
function isUnderSubmodule(filePath, submoduleDirs) {
|
|
510
|
-
return submoduleDirs.some(
|
|
511
|
-
(dir) => filePath === dir || filePath.startsWith(dir + path3.sep)
|
|
512
|
-
);
|
|
514
|
+
return submoduleDirs.some((dir) => filePath === dir || filePath.startsWith(dir + path3.sep));
|
|
513
515
|
}
|
|
514
516
|
function printDiff(file, original, updated) {
|
|
515
517
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,64 +1,74 @@
|
|
|
1
1
|
// index.ts
|
|
2
|
-
import path3 from "path";
|
|
3
2
|
import { existsSync as existsSync2 } from "fs";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import path3 from "path";
|
|
4
5
|
|
|
5
|
-
// src/
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let namespaceImport;
|
|
17
|
-
for (const s of node.specifiers) {
|
|
18
|
-
if (s.type === "ImportDefaultSpecifier") {
|
|
19
|
-
defaultImport = s.local.name;
|
|
20
|
-
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
21
|
-
namespaceImport = s.local.name;
|
|
22
|
-
} else if (s.type === "ImportSpecifier") {
|
|
23
|
-
const imported = s.imported;
|
|
24
|
-
const name = imported.type === "Identifier" ? imported.name : imported.value;
|
|
25
|
-
const localName = s.local.name;
|
|
26
|
-
const kind = s.importKind === "type" ? "type" : "value";
|
|
27
|
-
const named = { name, kind };
|
|
28
|
-
if (localName !== name) named.alias = localName;
|
|
29
|
-
namedImports.push(named);
|
|
30
|
-
}
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
function mergeConfigs(parent, child) {
|
|
10
|
+
const mergedGroups = [...parent.groups];
|
|
11
|
+
for (const childGroup of child.groups ?? []) {
|
|
12
|
+
const existingIdx = mergedGroups.findIndex((g) => g.name === childGroup.name);
|
|
13
|
+
if (existingIdx >= 0) {
|
|
14
|
+
mergedGroups[existingIdx] = childGroup;
|
|
15
|
+
} else {
|
|
16
|
+
mergedGroups.push(childGroup);
|
|
31
17
|
}
|
|
32
|
-
const importKind = node.importKind === "type" ? "type" : "value";
|
|
33
|
-
const isSideEffect = node.specifiers.length === 0 && importKind === "value";
|
|
34
|
-
const importNode = {
|
|
35
|
-
specifier,
|
|
36
|
-
namedImports,
|
|
37
|
-
isSideEffect,
|
|
38
|
-
importKind,
|
|
39
|
-
start: node.start,
|
|
40
|
-
end: node.end
|
|
41
|
-
};
|
|
42
|
-
if (defaultImport) importNode.defaultImport = defaultImport;
|
|
43
|
-
if (namespaceImport) importNode.namespaceImport = namespaceImport;
|
|
44
|
-
const attached = findAttachedComment(source, node.start, comments);
|
|
45
|
-
if (attached) importNode.attachedComment = attached;
|
|
46
|
-
nodes.push(importNode);
|
|
47
18
|
}
|
|
48
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
...parent,
|
|
21
|
+
...child,
|
|
22
|
+
groups: mergedGroups,
|
|
23
|
+
aliases: { ...parent.aliases ?? {}, ...child.aliases ?? {} }
|
|
24
|
+
};
|
|
49
25
|
}
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
26
|
+
async function loadConfig(configPath) {
|
|
27
|
+
const mod = await import(configPath);
|
|
28
|
+
const userConfig = mod.default ?? mod;
|
|
29
|
+
const configDir = path.dirname(configPath);
|
|
30
|
+
const config = { ...userConfig };
|
|
31
|
+
const tsconfig = loadTsConfig(configDir);
|
|
32
|
+
if (tsconfig) {
|
|
33
|
+
const opts = tsconfig.compilerOptions ?? {};
|
|
34
|
+
if (opts.baseUrl && !config.baseUrl) {
|
|
35
|
+
config.baseUrl = opts.baseUrl;
|
|
36
|
+
}
|
|
37
|
+
if (opts.paths) {
|
|
38
|
+
const aliases = { ...config.aliases ?? {} };
|
|
39
|
+
for (const [aliasPattern, targets] of Object.entries(
|
|
40
|
+
opts.paths
|
|
41
|
+
)) {
|
|
42
|
+
const alias = aliasPattern.replace(/\/\*$/, "");
|
|
43
|
+
const target = (targets[0] ?? "").replace(/\/\*$/, "");
|
|
44
|
+
if (!aliases[alias]) aliases[alias] = target;
|
|
45
|
+
}
|
|
46
|
+
config.aliases = aliases;
|
|
59
47
|
}
|
|
60
48
|
}
|
|
61
|
-
|
|
49
|
+
if (userConfig.extends) {
|
|
50
|
+
const parentPath = path.resolve(configDir, userConfig.extends);
|
|
51
|
+
if (!existsSync(parentPath)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`apotheke: extended config not found: ${parentPath}
|
|
54
|
+
(referenced from ${configPath} via "extends": "${userConfig.extends}")`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const parent = await loadConfig(parentPath);
|
|
58
|
+
return mergeConfigs(parent, config);
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
function loadTsConfig(dir) {
|
|
63
|
+
const tsconfigPath = path.join(dir, "tsconfig.json");
|
|
64
|
+
if (!existsSync(tsconfigPath)) return null;
|
|
65
|
+
try {
|
|
66
|
+
const text = readFileSync(tsconfigPath, "utf8");
|
|
67
|
+
const cleaned = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
68
|
+
return JSON.parse(cleaned);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
// src/deduplicator.ts
|
|
@@ -88,7 +98,7 @@ function deduplicateImports(imports) {
|
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
// src/grouper.ts
|
|
91
|
-
import
|
|
101
|
+
import path2 from "path";
|
|
92
102
|
function groupImports(imports, config, options = {}) {
|
|
93
103
|
const buckets = /* @__PURE__ */ new Map();
|
|
94
104
|
for (const node of imports) {
|
|
@@ -124,7 +134,7 @@ function assignGroup(node, config, options) {
|
|
|
124
134
|
function resolveCanonicalPath(specifier, config, options) {
|
|
125
135
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
126
136
|
const base = options.fileDir ?? process.cwd();
|
|
127
|
-
return
|
|
137
|
+
return path2.resolve(base, specifier);
|
|
128
138
|
}
|
|
129
139
|
const aliases = config.aliases ?? {};
|
|
130
140
|
for (const [alias, target] of Object.entries(aliases)) {
|
|
@@ -132,7 +142,7 @@ function resolveCanonicalPath(specifier, config, options) {
|
|
|
132
142
|
if (specifier === alias || specifier.startsWith(prefix)) {
|
|
133
143
|
const rest = specifier.slice(prefix.length);
|
|
134
144
|
const root = options.rootDir ?? process.cwd();
|
|
135
|
-
const resolved =
|
|
145
|
+
const resolved = path2.resolve(root, target, rest);
|
|
136
146
|
return resolved;
|
|
137
147
|
}
|
|
138
148
|
}
|
|
@@ -169,21 +179,63 @@ function globToRegex(pattern) {
|
|
|
169
179
|
return new RegExp(re + "$");
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
// src/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
// src/parser.ts
|
|
183
|
+
import { parseSync } from "oxc-parser";
|
|
184
|
+
function parseImports(source) {
|
|
185
|
+
const result = parseSync("file.tsx", source);
|
|
186
|
+
const comments = result.comments;
|
|
187
|
+
const nodes = [];
|
|
188
|
+
for (const node of result.program.body) {
|
|
189
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
190
|
+
const specifier = node.source.value;
|
|
191
|
+
let defaultImport;
|
|
192
|
+
const namedImports = [];
|
|
193
|
+
let namespaceImport;
|
|
194
|
+
for (const s of node.specifiers) {
|
|
195
|
+
if (s.type === "ImportDefaultSpecifier") {
|
|
196
|
+
defaultImport = s.local.name;
|
|
197
|
+
} else if (s.type === "ImportNamespaceSpecifier") {
|
|
198
|
+
namespaceImport = s.local.name;
|
|
199
|
+
} else if (s.type === "ImportSpecifier") {
|
|
200
|
+
const imported = s.imported;
|
|
201
|
+
const name = imported.type === "Identifier" ? imported.name : imported.value;
|
|
202
|
+
const localName = s.local.name;
|
|
203
|
+
const kind = s.importKind === "type" ? "type" : "value";
|
|
204
|
+
const named = { name, kind };
|
|
205
|
+
if (localName !== name) named.alias = localName;
|
|
206
|
+
namedImports.push(named);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const importKind = node.importKind === "type" ? "type" : "value";
|
|
210
|
+
const isSideEffect = node.specifiers.length === 0 && importKind === "value";
|
|
211
|
+
const importNode = {
|
|
212
|
+
specifier,
|
|
213
|
+
namedImports,
|
|
214
|
+
isSideEffect,
|
|
215
|
+
importKind,
|
|
216
|
+
start: node.start,
|
|
217
|
+
end: node.end
|
|
218
|
+
};
|
|
219
|
+
if (defaultImport) importNode.defaultImport = defaultImport;
|
|
220
|
+
if (namespaceImport) importNode.namespaceImport = namespaceImport;
|
|
221
|
+
const attached = findAttachedComment(source, node.start, comments);
|
|
222
|
+
if (attached) importNode.attachedComment = attached;
|
|
223
|
+
nodes.push(importNode);
|
|
224
|
+
}
|
|
225
|
+
return nodes;
|
|
179
226
|
}
|
|
180
|
-
function
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
function findAttachedComment(source, importStart, comments) {
|
|
228
|
+
const linesBefore = source.slice(0, importStart).split("\n");
|
|
229
|
+
const importLine = linesBefore.length - 1;
|
|
230
|
+
for (const comment of comments) {
|
|
231
|
+
if (comment.type !== "Line") continue;
|
|
232
|
+
const commentLines = source.slice(0, comment.end).split("\n");
|
|
233
|
+
const commentLine = commentLines.length - 1;
|
|
234
|
+
if (commentLine === importLine - 1) {
|
|
235
|
+
return `//${comment.value}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
187
239
|
}
|
|
188
240
|
|
|
189
241
|
// src/printer.ts
|
|
@@ -235,6 +287,23 @@ function buildImportStatement(node, q) {
|
|
|
235
287
|
return `import ${typePrefix}${parts.join(", ")} from ${q}${node.specifier}${q};`;
|
|
236
288
|
}
|
|
237
289
|
|
|
290
|
+
// src/sorter.ts
|
|
291
|
+
function sortGroup(imports) {
|
|
292
|
+
return [...imports].sort((a, b) => {
|
|
293
|
+
if (a.importKind === "type" && b.importKind !== "type") return -1;
|
|
294
|
+
if (a.importKind !== "type" && b.importKind === "type") return 1;
|
|
295
|
+
return a.specifier.localeCompare(b.specifier);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function sortNamedImports(node) {
|
|
299
|
+
const sorted = [...node.namedImports].sort((a, b) => {
|
|
300
|
+
if (a.kind === "type" && b.kind !== "type") return -1;
|
|
301
|
+
if (a.kind !== "type" && b.kind === "type") return 1;
|
|
302
|
+
return a.name.localeCompare(b.name);
|
|
303
|
+
});
|
|
304
|
+
return { ...node, namedImports: sorted };
|
|
305
|
+
}
|
|
306
|
+
|
|
238
307
|
// src/format.ts
|
|
239
308
|
function collectOrphanSegments(source, imports) {
|
|
240
309
|
const orphans = [];
|
|
@@ -282,76 +351,7 @@ function formatImports(source, config, options = {}) {
|
|
|
282
351
|
return before + newImportBlock + "\n" + orphanSuffix + after;
|
|
283
352
|
}
|
|
284
353
|
|
|
285
|
-
// src/config.ts
|
|
286
|
-
import path2 from "path";
|
|
287
|
-
import { existsSync, readFileSync } from "fs";
|
|
288
|
-
function mergeConfigs(parent, child) {
|
|
289
|
-
const mergedGroups = [...parent.groups];
|
|
290
|
-
for (const childGroup of child.groups ?? []) {
|
|
291
|
-
const existingIdx = mergedGroups.findIndex((g) => g.name === childGroup.name);
|
|
292
|
-
if (existingIdx >= 0) {
|
|
293
|
-
mergedGroups[existingIdx] = childGroup;
|
|
294
|
-
} else {
|
|
295
|
-
mergedGroups.push(childGroup);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
...parent,
|
|
300
|
-
...child,
|
|
301
|
-
groups: mergedGroups,
|
|
302
|
-
aliases: { ...parent.aliases ?? {}, ...child.aliases ?? {} }
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
async function loadConfig(configPath) {
|
|
306
|
-
const mod = await import(configPath);
|
|
307
|
-
const userConfig = mod.default ?? mod;
|
|
308
|
-
const configDir = path2.dirname(configPath);
|
|
309
|
-
const config = { ...userConfig };
|
|
310
|
-
const tsconfig = loadTsConfig(configDir);
|
|
311
|
-
if (tsconfig) {
|
|
312
|
-
const opts = tsconfig.compilerOptions ?? {};
|
|
313
|
-
if (opts.baseUrl && !config.baseUrl) {
|
|
314
|
-
config.baseUrl = opts.baseUrl;
|
|
315
|
-
}
|
|
316
|
-
if (opts.paths) {
|
|
317
|
-
const aliases = { ...config.aliases ?? {} };
|
|
318
|
-
for (const [aliasPattern, targets] of Object.entries(
|
|
319
|
-
opts.paths
|
|
320
|
-
)) {
|
|
321
|
-
const alias = aliasPattern.replace(/\/\*$/, "");
|
|
322
|
-
const target = (targets[0] ?? "").replace(/\/\*$/, "");
|
|
323
|
-
if (!aliases[alias]) aliases[alias] = target;
|
|
324
|
-
}
|
|
325
|
-
config.aliases = aliases;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (userConfig.extends) {
|
|
329
|
-
const parentPath = path2.resolve(configDir, userConfig.extends);
|
|
330
|
-
if (!existsSync(parentPath)) {
|
|
331
|
-
throw new Error(
|
|
332
|
-
`apotheke: extended config not found: ${parentPath}
|
|
333
|
-
(referenced from ${configPath} via "extends": "${userConfig.extends}")`
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
const parent = await loadConfig(parentPath);
|
|
337
|
-
return mergeConfigs(parent, config);
|
|
338
|
-
}
|
|
339
|
-
return config;
|
|
340
|
-
}
|
|
341
|
-
function loadTsConfig(dir) {
|
|
342
|
-
const tsconfigPath = path2.join(dir, "tsconfig.json");
|
|
343
|
-
if (!existsSync(tsconfigPath)) return null;
|
|
344
|
-
try {
|
|
345
|
-
const text = readFileSync(tsconfigPath, "utf8");
|
|
346
|
-
const cleaned = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
347
|
-
return JSON.parse(cleaned);
|
|
348
|
-
} catch {
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
354
|
// index.ts
|
|
354
|
-
import { createRequire } from "module";
|
|
355
355
|
async function resolveConfig(filePath) {
|
|
356
356
|
const candidates = ["apotheke.config.mjs", "apotheke.config.js"];
|
|
357
357
|
let dir = path3.dirname(path3.resolve(filePath));
|
|
@@ -368,7 +368,12 @@ async function resolveConfig(filePath) {
|
|
|
368
368
|
`apotheke: no config found for ${filePath}. Create an apotheke.config.mjs in your project root.`
|
|
369
369
|
);
|
|
370
370
|
}
|
|
371
|
+
var DOCUMENT_PARSERS = /* @__PURE__ */ new Set(["markdown", "mdx"]);
|
|
372
|
+
function isDocumentSnippet(parentParser) {
|
|
373
|
+
return typeof parentParser === "string" && DOCUMENT_PARSERS.has(parentParser);
|
|
374
|
+
}
|
|
371
375
|
async function preprocess(text, options) {
|
|
376
|
+
if (isDocumentSnippet(options.parentParser)) return text;
|
|
372
377
|
try {
|
|
373
378
|
const config = await resolveConfig(options.filepath ?? process.cwd());
|
|
374
379
|
const fileDir = options.filepath ? path3.dirname(path3.resolve(options.filepath)) : void 0;
|
|
@@ -436,7 +441,10 @@ function makeParser(pluginPath, parserName) {
|
|
|
436
441
|
async preprocess(text, opts) {
|
|
437
442
|
const base = getBase(opts?.filepath);
|
|
438
443
|
const basePreprocess = base?.preprocess;
|
|
439
|
-
const after = basePreprocess ? await basePreprocess(
|
|
444
|
+
const after = basePreprocess ? await basePreprocess(
|
|
445
|
+
text,
|
|
446
|
+
opts
|
|
447
|
+
) : text;
|
|
440
448
|
return preprocess(after, opts);
|
|
441
449
|
}
|
|
442
450
|
};
|
|
@@ -449,5 +457,6 @@ var parsers = {
|
|
|
449
457
|
};
|
|
450
458
|
export {
|
|
451
459
|
formatImports,
|
|
460
|
+
isDocumentSnippet,
|
|
452
461
|
parsers
|
|
453
462
|
};
|
package/package.json
CHANGED
|
@@ -1,48 +1,67 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
2
|
+
"name": "apotheke",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prettier for imports — deterministic import organization as a Prettier plugin or CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Marco Raffaello",
|
|
8
|
+
"homepage": "https://marcoraffaello.com/apotheke",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"imports",
|
|
11
|
+
"import-sorter",
|
|
12
|
+
"organize-imports",
|
|
13
|
+
"prettier",
|
|
14
|
+
"prettier-plugin",
|
|
15
|
+
"typescript",
|
|
16
|
+
"javascript",
|
|
17
|
+
"codemod",
|
|
18
|
+
"monorepo"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/mRaffaello/apotheke",
|
|
23
|
+
"directory": "packages/apotheke"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/mRaffaello/apotheke/issues"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"bin": {
|
|
34
|
+
"apotheke": "./dist/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"SKILL.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"lint:fix": "eslint . --fix",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:unit": "vitest run tests/unit",
|
|
47
|
+
"test:e2e": "vitest run tests/e2e/e2e.test.ts"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^25.6.0",
|
|
51
|
+
"tsup": "^8.5.1",
|
|
52
|
+
"typescript": "~5.9.3",
|
|
53
|
+
"vitest": "^4.1.5"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"prettier": ">=3"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"prettier": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"fast-glob": "^3.3.3",
|
|
65
|
+
"oxc-parser": "^0.129.0"
|
|
35
66
|
}
|
|
36
|
-
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"fast-glob": "^3.3.3",
|
|
39
|
-
"oxc-parser": "^0.129.0"
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "tsup",
|
|
43
|
-
"typecheck": "tsc --noEmit",
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"test:unit": "vitest run tests/unit",
|
|
46
|
-
"test:e2e": "vitest run tests/e2e/e2e.test.ts"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
67
|
+
}
|