harnery 0.2.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../src/commands/outline.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAoCnD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwBhF"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../src/commands/outline.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAqDnD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAwBhF"}
@@ -1,7 +1,21 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { extname, resolve } from "node:path";
3
- import * as ts from "typescript";
4
3
  import { resolveBinName } from "../core/config.js";
4
+ /**
5
+ * `typescript` is a devDependency, not a runtime dependency, so it is loaded
6
+ * lazily here and only when outlining a TS/JS file. A static top-level import
7
+ * would make the whole CLI fail to boot for an end user who installed harnery
8
+ * without dev deps (every command is registered at startup). PHP/Python
9
+ * outlines use regex and never touch this.
10
+ */
11
+ async function loadTypeScript() {
12
+ try {
13
+ return (await import("typescript"));
14
+ }
15
+ catch {
16
+ throw new Error("outlining TS/JS files needs the `typescript` package; install it (npm i -D typescript) or use outline on PHP/Python files");
17
+ }
18
+ }
5
19
  export function registerOutlineCommand(program, emit) {
6
20
  program
7
21
  .command("outline <file>")
@@ -37,7 +51,7 @@ async function runOutline(file, opts) {
37
51
  let parsed;
38
52
  if ([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"].includes(ext)) {
39
53
  language = ext.slice(1);
40
- parsed = outlineTypeScript(content, absPath, ext);
54
+ parsed = await outlineTypeScript(content, absPath, ext);
41
55
  }
42
56
  else if (ext === ".php") {
43
57
  language = "php";
@@ -64,7 +78,8 @@ async function runOutline(file, opts) {
64
78
  symbols: parsed.symbols,
65
79
  };
66
80
  }
67
- function outlineTypeScript(content, path, ext) {
81
+ async function outlineTypeScript(content, path, ext) {
82
+ const ts = await loadTypeScript();
68
83
  const isTsx = ext === ".tsx" || ext === ".jsx";
69
84
  const scriptKind = isTsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
70
85
  const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, scriptKind);
@@ -101,7 +116,7 @@ function outlineTypeScript(content, path, ext) {
101
116
  symbols.push({
102
117
  kind: "function",
103
118
  name: stmt.name.text,
104
- signature: renderFnSig(stmt, content, source),
119
+ signature: renderFnSig(ts, stmt, content, source),
105
120
  line: lineOf(stmt),
106
121
  exported,
107
122
  });
@@ -117,7 +132,7 @@ function outlineTypeScript(content, path, ext) {
117
132
  members.push({
118
133
  kind: "method",
119
134
  name: member.name.text || text(member.name),
120
- signature: renderFnSig(member, content, source),
135
+ signature: renderFnSig(ts, member, content, source),
121
136
  line: lineOf(member),
122
137
  });
123
138
  }
@@ -125,7 +140,7 @@ function outlineTypeScript(content, path, ext) {
125
140
  members.push({
126
141
  kind: "method",
127
142
  name: "constructor",
128
- signature: renderFnSig(member, content, source),
143
+ signature: renderFnSig(ts, member, content, source),
129
144
  line: lineOf(member),
130
145
  });
131
146
  }
@@ -165,7 +180,7 @@ function outlineTypeScript(content, path, ext) {
165
180
  members.push({
166
181
  kind: "method",
167
182
  name: member.name.text || text(member.name),
168
- signature: renderFnSig(member, content, source),
183
+ signature: renderFnSig(ts, member, content, source),
169
184
  line: lineOf(member),
170
185
  });
171
186
  }
@@ -216,7 +231,7 @@ function outlineTypeScript(content, path, ext) {
216
231
  sig = `: ${text(decl.type)}`;
217
232
  else if (decl.initializer &&
218
233
  (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
219
- sig = renderFnSig(decl.initializer, content, source);
234
+ sig = renderFnSig(ts, decl.initializer, content, source);
220
235
  }
221
236
  symbols.push({
222
237
  kind,
@@ -260,7 +275,7 @@ function outlineTypeScript(content, path, ext) {
260
275
  }
261
276
  return { imports, symbols };
262
277
  }
263
- function renderFnSig(fn, content, source) {
278
+ function renderFnSig(ts, fn, content, source) {
264
279
  const params = (fn.parameters || [])
265
280
  .map((p) => {
266
281
  const name = ts.isIdentifier(p.name)
@@ -17,9 +17,9 @@ export function registerReadCommand(program, emitParam) {
17
17
  .option("--selector <css>", "Use this CSS selector instead of Readability (fallback when extraction misses content)")
18
18
  .option("--raw", "Output cleaned HTML instead of markdown (debugging)")
19
19
  .option("--max-chars <n>", "Truncate output to N characters (0 = disable)", "100000")
20
- .action((htmlFile, opts) => {
20
+ .action(async (htmlFile, opts) => {
21
21
  try {
22
- runRead(htmlFile, opts);
22
+ await runRead(htmlFile, opts);
23
23
  }
24
24
  catch (err) {
25
25
  const msg = err instanceof Error ? err.message : String(err);
@@ -28,9 +28,9 @@ export function registerReadCommand(program, emitParam) {
28
28
  }
29
29
  });
30
30
  }
31
- function runRead(htmlFile, opts) {
31
+ async function runRead(htmlFile, opts) {
32
32
  const input = htmlFile && htmlFile !== "-" ? readFileSync(htmlFile, "utf-8") : readFileSync(0, "utf-8");
33
- const result = htmlToMarkdown(input, {
33
+ const result = await htmlToMarkdown(input, {
34
34
  url: opts.url,
35
35
  selector: opts.selector,
36
36
  raw: opts.raw,
@@ -3,6 +3,13 @@
3
3
  * Turndown (HTML → markdown).
4
4
  *
5
5
  * The library backing the `read` command.
6
+ *
7
+ * jsdom, @mozilla/readability, and turndown are heavy and (jsdom's tree)
8
+ * ESM-fragile, so they are loaded lazily inside `htmlToMarkdown` rather than
9
+ * at module top level. A static import would pull them into CLI startup (every
10
+ * command is registered eagerly), and jsdom's transitive `html-encoding-sniffer`
11
+ * crashes a plain `node` process via ERR_REQUIRE_ESM. Lazy-loading keeps the
12
+ * CLI booting for users who never touch `read`/`browse`.
6
13
  */
7
14
  export interface ReadabilityOptions {
8
15
  /** Base URL for resolving relative links. Default `http://local/`. */
@@ -28,5 +35,5 @@ export interface ReadabilityResult {
28
35
  * Throws if input is empty, the selector matches nothing, or Readability
29
36
  * fails to find a main article (caller should retry with `selector`).
30
37
  */
31
- export declare function htmlToMarkdown(html: string, opts?: ReadabilityOptions): ReadabilityResult;
38
+ export declare function htmlToMarkdown(html: string, opts?: ReadabilityOptions): Promise<ReadabilityResult>;
32
39
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/readability/client.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AAEH,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,iBAAiB,CAuD7F"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/readability/client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAID;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CA8D5B"}
@@ -1,8 +1,4 @@
1
1
  /// <reference path="./turndown-plugin-gfm.d.ts" />
2
- import { Readability } from "@mozilla/readability";
3
- import { JSDOM } from "jsdom";
4
- import TurndownService from "turndown";
5
- import { tables } from "turndown-plugin-gfm";
6
2
  const DEFAULT_MAX_CHARS = 100_000;
7
3
  /**
8
4
  * Convert raw HTML to clean markdown.
@@ -10,10 +6,16 @@ const DEFAULT_MAX_CHARS = 100_000;
10
6
  * Throws if input is empty, the selector matches nothing, or Readability
11
7
  * fails to find a main article (caller should retry with `selector`).
12
8
  */
13
- export function htmlToMarkdown(html, opts = {}) {
9
+ export async function htmlToMarkdown(html, opts = {}) {
14
10
  if (!html.trim()) {
15
11
  throw new Error("Empty HTML input");
16
12
  }
13
+ const [{ Readability }, { JSDOM }, { default: TurndownService }, { tables }] = await Promise.all([
14
+ import("@mozilla/readability"),
15
+ import("jsdom"),
16
+ import("turndown"),
17
+ import("turndown-plugin-gfm"),
18
+ ]);
17
19
  const dom = new JSDOM(html, { url: opts.url ?? "http://local/" });
18
20
  const doc = dom.window.document;
19
21
  preprocessDom(doc);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Multi-agent coordination + harness adapters + portable CLI utilities for Claude Code / Cursor / Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -1,10 +1,27 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { extname, resolve } from "node:path";
3
3
  import type { Command } from "commander";
4
- import * as ts from "typescript";
4
+ import type * as TS from "typescript";
5
5
  import type { EmitContext } from "../commander.ts";
6
6
  import { resolveBinName } from "../core/config.ts";
7
7
 
8
+ /**
9
+ * `typescript` is a devDependency, not a runtime dependency, so it is loaded
10
+ * lazily here and only when outlining a TS/JS file. A static top-level import
11
+ * would make the whole CLI fail to boot for an end user who installed harnery
12
+ * without dev deps (every command is registered at startup). PHP/Python
13
+ * outlines use regex and never touch this.
14
+ */
15
+ async function loadTypeScript(): Promise<typeof TS> {
16
+ try {
17
+ return (await import("typescript")) as unknown as typeof TS;
18
+ } catch {
19
+ throw new Error(
20
+ "outlining TS/JS files needs the `typescript` package; install it (npm i -D typescript) or use outline on PHP/Python files",
21
+ );
22
+ }
23
+ }
24
+
8
25
  /**
9
26
  * `harn outline <file>`: print the structural skeleton of a code file (imports
10
27
  * + top-level decls + line numbers). TS/JS/TSX/JSX use the TypeScript compiler
@@ -77,7 +94,7 @@ async function runOutline(file: string, opts: OutlineOpts): Promise<OutlineResul
77
94
 
78
95
  if ([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"].includes(ext)) {
79
96
  language = ext.slice(1);
80
- parsed = outlineTypeScript(content, absPath, ext);
97
+ parsed = await outlineTypeScript(content, absPath, ext);
81
98
  } else if (ext === ".php") {
82
99
  language = "php";
83
100
  parsed = outlinePhp(content);
@@ -105,11 +122,12 @@ async function runOutline(file: string, opts: OutlineOpts): Promise<OutlineResul
105
122
  };
106
123
  }
107
124
 
108
- function outlineTypeScript(
125
+ async function outlineTypeScript(
109
126
  content: string,
110
127
  path: string,
111
128
  ext: string,
112
- ): { imports: string[]; symbols: SymbolEntry[] } {
129
+ ): Promise<{ imports: string[]; symbols: SymbolEntry[] }> {
130
+ const ts = await loadTypeScript();
113
131
  const isTsx = ext === ".tsx" || ext === ".jsx";
114
132
  const scriptKind = isTsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
115
133
  const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, scriptKind);
@@ -117,18 +135,18 @@ function outlineTypeScript(
117
135
  const imports: string[] = [];
118
136
  const symbols: SymbolEntry[] = [];
119
137
 
120
- const lineOf = (node: ts.Node) =>
138
+ const lineOf = (node: TS.Node) =>
121
139
  source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
122
- const text = (node: ts.Node) =>
140
+ const text = (node: TS.Node) =>
123
141
  content.slice(node.getStart(source), node.getEnd()).replace(/\s+/g, " ").trim();
124
- const isExported = (node: ts.Node): boolean => {
125
- const mods = (node as { modifiers?: ReadonlyArray<ts.ModifierLike> }).modifiers;
142
+ const isExported = (node: TS.Node): boolean => {
143
+ const mods = (node as { modifiers?: ReadonlyArray<TS.ModifierLike> }).modifiers;
126
144
  return mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
127
145
  };
128
146
 
129
147
  for (const stmt of source.statements) {
130
148
  if (ts.isImportDeclaration(stmt)) {
131
- const moduleSpec = (stmt.moduleSpecifier as ts.StringLiteral).text;
149
+ const moduleSpec = (stmt.moduleSpecifier as TS.StringLiteral).text;
132
150
  const clause = stmt.importClause;
133
151
  const isTypeOnly = clause?.isTypeOnly ? "type " : "";
134
152
  let what = "";
@@ -151,7 +169,7 @@ function outlineTypeScript(
151
169
  symbols.push({
152
170
  kind: "function",
153
171
  name: stmt.name.text,
154
- signature: renderFnSig(stmt, content, source),
172
+ signature: renderFnSig(ts, stmt, content, source),
155
173
  line: lineOf(stmt),
156
174
  exported,
157
175
  });
@@ -168,21 +186,21 @@ function outlineTypeScript(
168
186
  if (ts.isMethodDeclaration(member) && member.name) {
169
187
  members.push({
170
188
  kind: "method",
171
- name: (member.name as ts.Identifier).text || text(member.name),
172
- signature: renderFnSig(member, content, source),
189
+ name: (member.name as TS.Identifier).text || text(member.name),
190
+ signature: renderFnSig(ts, member, content, source),
173
191
  line: lineOf(member),
174
192
  });
175
193
  } else if (ts.isConstructorDeclaration(member)) {
176
194
  members.push({
177
195
  kind: "method",
178
196
  name: "constructor",
179
- signature: renderFnSig(member, content, source),
197
+ signature: renderFnSig(ts, member, content, source),
180
198
  line: lineOf(member),
181
199
  });
182
200
  } else if (ts.isPropertyDeclaration(member) && member.name) {
183
201
  members.push({
184
202
  kind: "property",
185
- name: (member.name as ts.Identifier).text || text(member.name),
203
+ name: (member.name as TS.Identifier).text || text(member.name),
186
204
  signature: member.type ? `: ${text(member.type)}` : "",
187
205
  line: lineOf(member),
188
206
  });
@@ -205,15 +223,15 @@ function outlineTypeScript(
205
223
  if (ts.isPropertySignature(member) && member.name) {
206
224
  members.push({
207
225
  kind: "property",
208
- name: (member.name as ts.Identifier).text || text(member.name),
226
+ name: (member.name as TS.Identifier).text || text(member.name),
209
227
  signature: member.type ? `: ${text(member.type)}` : "",
210
228
  line: lineOf(member),
211
229
  });
212
230
  } else if (ts.isMethodSignature(member) && member.name) {
213
231
  members.push({
214
232
  kind: "method",
215
- name: (member.name as ts.Identifier).text || text(member.name),
216
- signature: renderFnSig(member, content, source),
233
+ name: (member.name as TS.Identifier).text || text(member.name),
234
+ signature: renderFnSig(ts, member, content, source),
217
235
  line: lineOf(member),
218
236
  });
219
237
  }
@@ -243,7 +261,7 @@ function outlineTypeScript(
243
261
  exported,
244
262
  members: stmt.members.map((m) => ({
245
263
  kind: "enumMember",
246
- name: (m.name as ts.Identifier).text || text(m.name),
264
+ name: (m.name as TS.Identifier).text || text(m.name),
247
265
  line: lineOf(m),
248
266
  })),
249
267
  });
@@ -261,7 +279,7 @@ function outlineTypeScript(
261
279
  decl.initializer &&
262
280
  (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
263
281
  ) {
264
- sig = renderFnSig(decl.initializer, content, source);
282
+ sig = renderFnSig(ts, decl.initializer, content, source);
265
283
  }
266
284
  symbols.push({
267
285
  kind,
@@ -275,7 +293,7 @@ function outlineTypeScript(
275
293
  if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
276
294
  const names = stmt.exportClause.elements.map((e) => e.name.text).join(", ");
277
295
  const fromText = stmt.moduleSpecifier
278
- ? ` from "${(stmt.moduleSpecifier as ts.StringLiteral).text}"`
296
+ ? ` from "${(stmt.moduleSpecifier as TS.StringLiteral).text}"`
279
297
  : "";
280
298
  symbols.push({
281
299
  kind: "re-export",
@@ -286,7 +304,7 @@ function outlineTypeScript(
286
304
  } else if (stmt.moduleSpecifier) {
287
305
  symbols.push({
288
306
  kind: "re-export",
289
- name: `* from "${(stmt.moduleSpecifier as ts.StringLiteral).text}"`,
307
+ name: `* from "${(stmt.moduleSpecifier as TS.StringLiteral).text}"`,
290
308
  line: lineOf(stmt),
291
309
  exported: true,
292
310
  });
@@ -294,7 +312,7 @@ function outlineTypeScript(
294
312
  } else if (ts.isModuleDeclaration(stmt) && stmt.name) {
295
313
  symbols.push({
296
314
  kind: "namespace",
297
- name: (stmt.name as ts.Identifier).text || text(stmt.name),
315
+ name: (stmt.name as TS.Identifier).text || text(stmt.name),
298
316
  line: lineOf(stmt),
299
317
  exported,
300
318
  });
@@ -305,9 +323,10 @@ function outlineTypeScript(
305
323
  }
306
324
 
307
325
  function renderFnSig(
308
- fn: ts.FunctionLikeDeclarationBase | ts.MethodSignature,
326
+ ts: typeof TS,
327
+ fn: TS.FunctionLikeDeclarationBase | TS.MethodSignature,
309
328
  content: string,
310
- source: ts.SourceFile,
329
+ source: TS.SourceFile,
311
330
  ): string {
312
331
  const params = (fn.parameters || [])
313
332
  .map((p) => {
@@ -26,9 +26,9 @@ export function registerReadCommand(program: Command, emitParam: EmitContext): v
26
26
  )
27
27
  .option("--raw", "Output cleaned HTML instead of markdown (debugging)")
28
28
  .option("--max-chars <n>", "Truncate output to N characters (0 = disable)", "100000")
29
- .action((htmlFile: string | undefined, opts: ReadOpts) => {
29
+ .action(async (htmlFile: string | undefined, opts: ReadOpts) => {
30
30
  try {
31
- runRead(htmlFile, opts);
31
+ await runRead(htmlFile, opts);
32
32
  } catch (err: unknown) {
33
33
  const msg = err instanceof Error ? err.message : String(err);
34
34
  emit.error({ code: "read_error", message: msg });
@@ -45,10 +45,10 @@ interface ReadOpts {
45
45
  maxChars: string;
46
46
  }
47
47
 
48
- function runRead(htmlFile: string | undefined, opts: ReadOpts): void {
48
+ async function runRead(htmlFile: string | undefined, opts: ReadOpts): Promise<void> {
49
49
  const input =
50
50
  htmlFile && htmlFile !== "-" ? readFileSync(htmlFile, "utf-8") : readFileSync(0, "utf-8");
51
- const result = htmlToMarkdown(input, {
51
+ const result = await htmlToMarkdown(input, {
52
52
  url: opts.url,
53
53
  selector: opts.selector,
54
54
  raw: opts.raw,
@@ -1,14 +1,17 @@
1
1
  /// <reference path="./turndown-plugin-gfm.d.ts" />
2
- import { Readability } from "@mozilla/readability";
3
- import { JSDOM } from "jsdom";
4
- import TurndownService from "turndown";
5
- import { tables } from "turndown-plugin-gfm";
6
2
 
7
3
  /**
8
4
  * HTML → clean markdown via Readability (main-content extraction) +
9
5
  * Turndown (HTML → markdown).
10
6
  *
11
7
  * The library backing the `read` command.
8
+ *
9
+ * jsdom, @mozilla/readability, and turndown are heavy and (jsdom's tree)
10
+ * ESM-fragile, so they are loaded lazily inside `htmlToMarkdown` rather than
11
+ * at module top level. A static import would pull them into CLI startup (every
12
+ * command is registered eagerly), and jsdom's transitive `html-encoding-sniffer`
13
+ * crashes a plain `node` process via ERR_REQUIRE_ESM. Lazy-loading keeps the
14
+ * CLI booting for users who never touch `read`/`browse`.
12
15
  */
13
16
 
14
17
  export interface ReadabilityOptions {
@@ -39,11 +42,21 @@ const DEFAULT_MAX_CHARS = 100_000;
39
42
  * Throws if input is empty, the selector matches nothing, or Readability
40
43
  * fails to find a main article (caller should retry with `selector`).
41
44
  */
42
- export function htmlToMarkdown(html: string, opts: ReadabilityOptions = {}): ReadabilityResult {
45
+ export async function htmlToMarkdown(
46
+ html: string,
47
+ opts: ReadabilityOptions = {},
48
+ ): Promise<ReadabilityResult> {
43
49
  if (!html.trim()) {
44
50
  throw new Error("Empty HTML input");
45
51
  }
46
52
 
53
+ const [{ Readability }, { JSDOM }, { default: TurndownService }, { tables }] = await Promise.all([
54
+ import("@mozilla/readability"),
55
+ import("jsdom"),
56
+ import("turndown"),
57
+ import("turndown-plugin-gfm"),
58
+ ]);
59
+
47
60
  const dom = new JSDOM(html, { url: opts.url ?? "http://local/" });
48
61
  const doc = dom.window.document;
49
62
  preprocessDom(doc);