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/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/parser.ts
6
- import { parseSync } from "oxc-parser";
7
- function parseImports(source) {
8
- const result = parseSync("file.tsx", source);
9
- const comments = result.comments;
10
- const nodes = [];
11
- for (const node of result.program.body) {
12
- if (node.type !== "ImportDeclaration") continue;
13
- const specifier = node.source.value;
14
- let defaultImport;
15
- const namedImports = [];
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 nodes;
19
+ return {
20
+ ...parent,
21
+ ...child,
22
+ groups: mergedGroups,
23
+ aliases: { ...parent.aliases ?? {}, ...child.aliases ?? {} }
24
+ };
49
25
  }
50
- function findAttachedComment(source, importStart, comments) {
51
- const linesBefore = source.slice(0, importStart).split("\n");
52
- const importLine = linesBefore.length - 1;
53
- for (const comment of comments) {
54
- if (comment.type !== "Line") continue;
55
- const commentLines = source.slice(0, comment.end).split("\n");
56
- const commentLine = commentLines.length - 1;
57
- if (commentLine === importLine - 1) {
58
- return `//${comment.value}`;
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
- return void 0;
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,9 @@ function deduplicateImports(imports) {
88
98
  }
89
99
 
90
100
  // src/grouper.ts
91
- import path from "path";
101
+ import path2 from "path";
102
+ var SIDE_EFFECTS_GROUP = "SideEffects";
103
+ var OTHERS_GROUP = "Others";
92
104
  function groupImports(imports, config, options = {}) {
93
105
  const buckets = /* @__PURE__ */ new Map();
94
106
  for (const node of imports) {
@@ -97,21 +109,21 @@ function groupImports(imports, config, options = {}) {
97
109
  buckets.get(groupName).push(node);
98
110
  }
99
111
  const result = [];
100
- if (buckets.has("SideEffects")) {
101
- result.push({ name: "SideEffects", imports: buckets.get("SideEffects") });
112
+ if (buckets.has(SIDE_EFFECTS_GROUP)) {
113
+ result.push({ name: SIDE_EFFECTS_GROUP, imports: buckets.get(SIDE_EFFECTS_GROUP) });
102
114
  }
103
115
  for (const group of config.groups) {
104
116
  if (buckets.has(group.name)) {
105
117
  result.push({ name: group.name, imports: buckets.get(group.name) });
106
118
  }
107
119
  }
108
- if (buckets.has("Others")) {
109
- result.push({ name: "Others", imports: buckets.get("Others") });
120
+ if (buckets.has(OTHERS_GROUP)) {
121
+ result.push({ name: OTHERS_GROUP, imports: buckets.get(OTHERS_GROUP) });
110
122
  }
111
123
  return result;
112
124
  }
113
125
  function assignGroup(node, config, options) {
114
- if (node.isSideEffect) return "SideEffects";
126
+ if (node.isSideEffect) return SIDE_EFFECTS_GROUP;
115
127
  const canonicalPath = resolveCanonicalPath(node.specifier, config, options);
116
128
  for (const group of config.groups) {
117
129
  for (const pattern of group.match) {
@@ -119,12 +131,12 @@ function assignGroup(node, config, options) {
119
131
  if (canonicalPath && matches(canonicalPath, pattern)) return group.name;
120
132
  }
121
133
  }
122
- return "Others";
134
+ return OTHERS_GROUP;
123
135
  }
124
136
  function resolveCanonicalPath(specifier, config, options) {
125
137
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
126
138
  const base = options.fileDir ?? process.cwd();
127
- return path.resolve(base, specifier);
139
+ return path2.resolve(base, specifier);
128
140
  }
129
141
  const aliases = config.aliases ?? {};
130
142
  for (const [alias, target] of Object.entries(aliases)) {
@@ -132,7 +144,7 @@ function resolveCanonicalPath(specifier, config, options) {
132
144
  if (specifier === alias || specifier.startsWith(prefix)) {
133
145
  const rest = specifier.slice(prefix.length);
134
146
  const root = options.rootDir ?? process.cwd();
135
- const resolved = path.resolve(root, target, rest);
147
+ const resolved = path2.resolve(root, target, rest);
136
148
  return resolved;
137
149
  }
138
150
  }
@@ -169,21 +181,63 @@ function globToRegex(pattern) {
169
181
  return new RegExp(re + "$");
170
182
  }
171
183
 
172
- // src/sorter.ts
173
- function sortGroup(imports) {
174
- return [...imports].sort((a, b) => {
175
- if (a.importKind === "type" && b.importKind !== "type") return -1;
176
- if (a.importKind !== "type" && b.importKind === "type") return 1;
177
- return a.specifier.localeCompare(b.specifier);
178
- });
184
+ // src/parser.ts
185
+ import { parseSync } from "oxc-parser";
186
+ function parseImports(source) {
187
+ const result = parseSync("file.tsx", source);
188
+ const comments = result.comments;
189
+ const nodes = [];
190
+ for (const node of result.program.body) {
191
+ if (node.type !== "ImportDeclaration") continue;
192
+ const specifier = node.source.value;
193
+ let defaultImport;
194
+ const namedImports = [];
195
+ let namespaceImport;
196
+ for (const s of node.specifiers) {
197
+ if (s.type === "ImportDefaultSpecifier") {
198
+ defaultImport = s.local.name;
199
+ } else if (s.type === "ImportNamespaceSpecifier") {
200
+ namespaceImport = s.local.name;
201
+ } else if (s.type === "ImportSpecifier") {
202
+ const imported = s.imported;
203
+ const name = imported.type === "Identifier" ? imported.name : imported.value;
204
+ const localName = s.local.name;
205
+ const kind = s.importKind === "type" ? "type" : "value";
206
+ const named = { name, kind };
207
+ if (localName !== name) named.alias = localName;
208
+ namedImports.push(named);
209
+ }
210
+ }
211
+ const importKind = node.importKind === "type" ? "type" : "value";
212
+ const isSideEffect = node.specifiers.length === 0 && importKind === "value";
213
+ const importNode = {
214
+ specifier,
215
+ namedImports,
216
+ isSideEffect,
217
+ importKind,
218
+ start: node.start,
219
+ end: node.end
220
+ };
221
+ if (defaultImport) importNode.defaultImport = defaultImport;
222
+ if (namespaceImport) importNode.namespaceImport = namespaceImport;
223
+ const attached = findAttachedComment(source, node.start, comments);
224
+ if (attached) importNode.attachedComment = attached;
225
+ nodes.push(importNode);
226
+ }
227
+ return nodes;
179
228
  }
180
- function sortNamedImports(node) {
181
- const sorted = [...node.namedImports].sort((a, b) => {
182
- if (a.kind === "type" && b.kind !== "type") return -1;
183
- if (a.kind !== "type" && b.kind === "type") return 1;
184
- return a.name.localeCompare(b.name);
185
- });
186
- return { ...node, namedImports: sorted };
229
+ function findAttachedComment(source, importStart, comments) {
230
+ const linesBefore = source.slice(0, importStart).split("\n");
231
+ const importLine = linesBefore.length - 1;
232
+ for (const comment of comments) {
233
+ if (comment.type !== "Line") continue;
234
+ const commentLines = source.slice(0, comment.end).split("\n");
235
+ const commentLine = commentLines.length - 1;
236
+ if (commentLine === importLine - 1) {
237
+ return `//${comment.value}`;
238
+ }
239
+ }
240
+ return void 0;
187
241
  }
188
242
 
189
243
  // src/printer.ts
@@ -235,6 +289,23 @@ function buildImportStatement(node, q) {
235
289
  return `import ${typePrefix}${parts.join(", ")} from ${q}${node.specifier}${q};`;
236
290
  }
237
291
 
292
+ // src/sorter.ts
293
+ function sortGroup(imports) {
294
+ return [...imports].sort((a, b) => {
295
+ if (a.importKind === "type" && b.importKind !== "type") return -1;
296
+ if (a.importKind !== "type" && b.importKind === "type") return 1;
297
+ return a.specifier.localeCompare(b.specifier);
298
+ });
299
+ }
300
+ function sortNamedImports(node) {
301
+ const sorted = [...node.namedImports].sort((a, b) => {
302
+ if (a.kind === "type" && b.kind !== "type") return -1;
303
+ if (a.kind !== "type" && b.kind === "type") return 1;
304
+ return a.name.localeCompare(b.name);
305
+ });
306
+ return { ...node, namedImports: sorted };
307
+ }
308
+
238
309
  // src/format.ts
239
310
  function collectOrphanSegments(source, imports) {
240
311
  const orphans = [];
@@ -263,7 +334,9 @@ function formatImports(source, config, options = {}) {
263
334
  const deduped = deduplicateImports(clean);
264
335
  const sorted = deduped.map(sortNamedImports);
265
336
  const grouped = groupImports(sorted, config, options);
266
- const sortedGroups = grouped.map((g) => ({ ...g, imports: sortGroup(g.imports) }));
337
+ const sortedGroups = grouped.map(
338
+ (g) => g.name === SIDE_EFFECTS_GROUP ? g : { ...g, imports: sortGroup(g.imports) }
339
+ );
267
340
  const q = detectQuoteChar(source);
268
341
  const newImportBlock = printGroups(sortedGroups, config, q);
269
342
  const firstImport = imports[0];
@@ -282,76 +355,7 @@ function formatImports(source, config, options = {}) {
282
355
  return before + newImportBlock + "\n" + orphanSuffix + after;
283
356
  }
284
357
 
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
358
  // index.ts
354
- import { createRequire } from "module";
355
359
  async function resolveConfig(filePath) {
356
360
  const candidates = ["apotheke.config.mjs", "apotheke.config.js"];
357
361
  let dir = path3.dirname(path3.resolve(filePath));
@@ -368,7 +372,12 @@ async function resolveConfig(filePath) {
368
372
  `apotheke: no config found for ${filePath}. Create an apotheke.config.mjs in your project root.`
369
373
  );
370
374
  }
375
+ var DOCUMENT_PARSERS = /* @__PURE__ */ new Set(["markdown", "mdx"]);
376
+ function isDocumentSnippet(parentParser) {
377
+ return typeof parentParser === "string" && DOCUMENT_PARSERS.has(parentParser);
378
+ }
371
379
  async function preprocess(text, options) {
380
+ if (isDocumentSnippet(options.parentParser)) return text;
372
381
  try {
373
382
  const config = await resolveConfig(options.filepath ?? process.cwd());
374
383
  const fileDir = options.filepath ? path3.dirname(path3.resolve(options.filepath)) : void 0;
@@ -436,7 +445,10 @@ function makeParser(pluginPath, parserName) {
436
445
  async preprocess(text, opts) {
437
446
  const base = getBase(opts?.filepath);
438
447
  const basePreprocess = base?.preprocess;
439
- const after = basePreprocess ? await basePreprocess(text, opts) : text;
448
+ const after = basePreprocess ? await basePreprocess(
449
+ text,
450
+ opts
451
+ ) : text;
440
452
  return preprocess(after, opts);
441
453
  }
442
454
  };
@@ -449,5 +461,6 @@ var parsers = {
449
461
  };
450
462
  export {
451
463
  formatImports,
464
+ isDocumentSnippet,
452
465
  parsers
453
466
  };
package/package.json CHANGED
@@ -1,48 +1,67 @@
1
1
  {
2
- "name": "apotheke",
3
- "version": "0.0.1",
4
- "description": "JavaScript/TypeScript import organizer — prettier plugin + CLI",
5
- "type": "module",
6
- "repository": {
7
- "url": "https://github.com/mRaffaello/apotheke"
8
- },
9
- "exports": {
10
- ".": "./dist/index.js"
11
- },
12
- "main": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
14
- "bin": {
15
- "apotheke": "./dist/cli.js"
16
- },
17
- "files": [
18
- "dist"
19
- ],
20
- "devDependencies": {
21
- "@commitlint/cli": "^20.5.3",
22
- "@commitlint/config-conventional": "^20.5.3",
23
- "@types/node": "^25.6.0",
24
- "husky": "^9.1.7",
25
- "prettier": "^3.8.3",
26
- "tsup": "^8.5.1",
27
- "vitest": "^4.1.5"
28
- },
29
- "peerDependencies": {
30
- "prettier": ">=3"
31
- },
32
- "peerDependenciesMeta": {
33
- "prettier": {
34
- "optional": true
2
+ "name": "apotheke",
3
+ "version": "0.1.1",
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
+ }