Skip to content

Commit 274e1e5

Browse files
claude: Refactor quarto-api with dependency inversion and registry pattern
Refactored src/core/quarto-api.ts (397 lines) into modular structure under src/core/api/ (13 files, 743 lines) to eliminate circular dependencies using dependency inversion and registry pattern. Architecture: - types.ts: Interface definitions using ONLY import type - registry.ts: Generic registry with eager validation - 9 namespace modules: crypto, console, markdown-regex, mapped-string, format, path, text, system, jupyter - register.ts: Side-effect aggregation module - index.ts: Main entry with lazy Proxy initialization Key features: - Zero circular dependencies via import type only in types.ts - Eager validation: All namespaces validated at startup - Lazy initialization: Proxy-based API creation on first access - Type safety: Full TypeScript checking maintained - API compatibility: Zero changes to QuartoAPI interface surface 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 25afc6f commit 274e1e5

21 files changed

+758
-407
lines changed

src/core/api/console.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// src/core/api/console.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { ConsoleNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import { completeMessage, withSpinner } from "../console.ts";
8+
import * as log from "../../deno_ral/log.ts";
9+
import type { LogMessageOptions } from "../log.ts";
10+
11+
// Register console namespace
12+
globalRegistry.register("console", (): ConsoleNamespace => {
13+
return {
14+
withSpinner,
15+
completeMessage,
16+
info: (message: string, options?: LogMessageOptions) =>
17+
log.info(message, options),
18+
warning: (message: string, options?: LogMessageOptions) =>
19+
log.warning(message, options),
20+
error: (message: string, options?: LogMessageOptions) =>
21+
log.error(message, options),
22+
};
23+
});

src/core/api/crypto.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// src/core/api/crypto.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { CryptoNamespace } from "./types.ts";
5+
6+
// Import implementation
7+
import { md5HashSync } from "../hash.ts";
8+
9+
// Register crypto namespace
10+
globalRegistry.register("crypto", (): CryptoNamespace => {
11+
return {
12+
md5Hash: md5HashSync,
13+
};
14+
});

src/core/api/format.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// src/core/api/format.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { FormatNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import {
8+
isHtmlCompatible,
9+
isHtmlDashboardOutput,
10+
isIpynbOutput,
11+
isLatexOutput,
12+
isMarkdownOutput,
13+
isPresentationOutput,
14+
} from "../../config/format.ts";
15+
import { isServerShiny, isServerShinyPython } from "../render.ts";
16+
17+
// Register format namespace
18+
globalRegistry.register("format", (): FormatNamespace => {
19+
return {
20+
isHtmlCompatible,
21+
isIpynbOutput,
22+
isLatexOutput,
23+
isMarkdownOutput,
24+
isPresentationOutput,
25+
isHtmlDashboardOutput: (format?: string) => !!isHtmlDashboardOutput(format),
26+
isServerShiny,
27+
isServerShinyPython,
28+
};
29+
});

src/core/api/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// src/core/api/index.ts
2+
//
3+
// Main entry point for QuartoAPI
4+
// This module exports the global quartoAPI instance and all related types
5+
6+
import { globalRegistry } from "./registry.ts";
7+
import type { QuartoAPI } from "./types.ts";
8+
9+
// Export all types for external use
10+
export type {
11+
ConsoleNamespace,
12+
CryptoNamespace,
13+
FormatNamespace,
14+
JupyterNamespace,
15+
MappedStringNamespace,
16+
MarkdownRegexNamespace,
17+
PathNamespace,
18+
QuartoAPI,
19+
SystemNamespace,
20+
TextNamespace,
21+
} from "./types.ts";
22+
23+
// Export registry utilities (mainly for testing)
24+
export {
25+
globalRegistry,
26+
QuartoAPIRegistry,
27+
RegistryFinalizedError,
28+
UnregisteredNamespaceError,
29+
} from "./registry.ts";
30+
31+
/**
32+
* The global QuartoAPI instance
33+
*
34+
* This is created lazily on first access via a getter.
35+
* The register.ts module (imported in src/quarto.ts) ensures all
36+
* namespaces are registered before any code accesses the API.
37+
*/
38+
let _quartoAPI: QuartoAPI | null = null;
39+
40+
export const quartoAPI = new Proxy({} as QuartoAPI, {
41+
get(_target, prop) {
42+
// Create API on first access
43+
if (_quartoAPI === null) {
44+
_quartoAPI = globalRegistry.createAPI();
45+
}
46+
return _quartoAPI[prop as keyof QuartoAPI];
47+
},
48+
});

src/core/api/jupyter.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// src/core/api/jupyter.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { JupyterNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import {
8+
executeResultEngineDependencies,
9+
executeResultIncludes,
10+
isJupyterNotebook,
11+
jupyterAssets,
12+
jupyterFromJSON,
13+
jupyterKernelspecFromMarkdown,
14+
jupyterToMarkdown,
15+
kJupyterNotebookExtensions,
16+
quartoMdToJupyter,
17+
} from "../jupyter/jupyter.ts";
18+
import {
19+
jupyterNotebookFiltered,
20+
markdownFromNotebookFile,
21+
markdownFromNotebookJSON,
22+
} from "../jupyter/jupyter-filters.ts";
23+
import { includesForJupyterWidgetDependencies } from "../jupyter/widgets.ts";
24+
import { pythonExec } from "../jupyter/exec.ts";
25+
import { jupyterCapabilities } from "../jupyter/capabilities.ts";
26+
import { jupyterKernelspecForLanguage } from "../jupyter/kernels.ts";
27+
import {
28+
jupyterCapabilitiesJson,
29+
jupyterCapabilitiesMessage,
30+
jupyterInstallationMessage,
31+
jupyterUnactivatedEnvMessage,
32+
pythonInstallationMessage,
33+
} from "../jupyter/jupyter-shared.ts";
34+
import {
35+
isJupyterPercentScript,
36+
markdownFromJupyterPercentScript,
37+
} from "../jupyter/percent.ts";
38+
import type { JupyterWidgetDependencies } from "../jupyter/types.ts";
39+
40+
// Register jupyter namespace
41+
globalRegistry.register("jupyter", (): JupyterNamespace => {
42+
return {
43+
// 1. Notebook Detection & Introspection
44+
isJupyterNotebook,
45+
isPercentScript: isJupyterPercentScript,
46+
notebookExtensions: kJupyterNotebookExtensions,
47+
kernelspecFromMarkdown: jupyterKernelspecFromMarkdown,
48+
kernelspecForLanguage: jupyterKernelspecForLanguage,
49+
fromJSON: jupyterFromJSON,
50+
51+
// 2. Notebook Conversion
52+
toMarkdown: jupyterToMarkdown,
53+
markdownFromNotebookFile,
54+
markdownFromNotebookJSON,
55+
percentScriptToMarkdown: markdownFromJupyterPercentScript,
56+
quartoMdToJupyter,
57+
58+
// 3. Notebook Processing & Assets
59+
notebookFiltered: jupyterNotebookFiltered,
60+
assets: jupyterAssets,
61+
widgetDependencyIncludes: includesForJupyterWidgetDependencies,
62+
resultIncludes: (
63+
tempDir: string,
64+
dependencies?: JupyterWidgetDependencies,
65+
) => {
66+
return executeResultIncludes(tempDir, dependencies) || {};
67+
},
68+
resultEngineDependencies: (dependencies?: JupyterWidgetDependencies) => {
69+
const result = executeResultEngineDependencies(dependencies);
70+
return result as Array<JupyterWidgetDependencies> | undefined;
71+
},
72+
73+
// 4. Runtime & Environment
74+
pythonExec,
75+
capabilities: jupyterCapabilities,
76+
capabilitiesMessage: jupyterCapabilitiesMessage,
77+
capabilitiesJson: jupyterCapabilitiesJson,
78+
installationMessage: jupyterInstallationMessage,
79+
unactivatedEnvMessage: jupyterUnactivatedEnvMessage,
80+
pythonInstallationMessage,
81+
};
82+
});

src/core/api/mapped-string.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// src/core/api/mapped-string.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { MappedStringNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import {
8+
asMappedString,
9+
mappedIndexToLineCol,
10+
mappedLines,
11+
mappedNormalizeNewlines,
12+
} from "../lib/mapped-text.ts";
13+
import { mappedStringFromFile } from "../mapped-text.ts";
14+
import type { MappedString } from "../lib/text-types.ts";
15+
16+
// Register mappedString namespace
17+
globalRegistry.register("mappedString", (): MappedStringNamespace => {
18+
return {
19+
fromString: asMappedString,
20+
fromFile: mappedStringFromFile,
21+
normalizeNewlines: mappedNormalizeNewlines,
22+
splitLines: (str: MappedString, keepNewLines?: boolean) => {
23+
return mappedLines(str, keepNewLines);
24+
},
25+
indexToLineCol: (str: MappedString, offset: number) => {
26+
const fn = mappedIndexToLineCol(str);
27+
return fn(offset);
28+
},
29+
};
30+
});

src/core/api/markdown-regex.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// src/core/api/markdown-regex.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { MarkdownRegexNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import { readYamlFromMarkdown } from "../yaml.ts";
8+
import {
9+
languagesInMarkdown,
10+
partitionMarkdown,
11+
} from "../pandoc/pandoc-partition.ts";
12+
import { breakQuartoMd } from "../lib/break-quarto-md.ts";
13+
14+
// Register markdownRegex namespace
15+
globalRegistry.register("markdownRegex", (): MarkdownRegexNamespace => {
16+
return {
17+
extractYaml: readYamlFromMarkdown,
18+
partition: partitionMarkdown,
19+
getLanguages: languagesInMarkdown,
20+
breakQuartoMd,
21+
};
22+
});

src/core/api/path.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// src/core/api/path.ts
2+
3+
import { globalRegistry } from "./registry.ts";
4+
import type { PathNamespace } from "./types.ts";
5+
6+
// Import implementations
7+
import {
8+
dirAndStem,
9+
isQmdFile,
10+
normalizePath,
11+
pathWithForwardSlashes,
12+
} from "../path.ts";
13+
import { quartoDataDir, quartoRuntimeDir } from "../appdirs.ts";
14+
import { resourcePath } from "../resources.ts";
15+
import { inputFilesDir } from "../render.ts";
16+
17+
// Register path namespace
18+
globalRegistry.register("path", (): PathNamespace => {
19+
return {
20+
absolute: normalizePath,
21+
toForwardSlashes: pathWithForwardSlashes,
22+
runtime: quartoRuntimeDir,
23+
resource: (...parts: string[]) => {
24+
if (parts.length === 0) {
25+
return resourcePath();
26+
} else if (parts.length === 1) {
27+
return resourcePath(parts[0]);
28+
} else {
29+
// Join multiple parts with the first one
30+
const joined = parts.join("/");
31+
return resourcePath(joined);
32+
}
33+
},
34+
dirAndStem,
35+
isQmdFile,
36+
inputFilesDir,
37+
dataDir: quartoDataDir,
38+
};
39+
});

src/core/api/register.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// src/core/api/register.ts
2+
//
3+
// Side-effect module that imports all namespace registrations
4+
// This module has no exports - its purpose is to trigger registration
5+
// of all QuartoAPI namespaces with the global registry.
6+
7+
import "./markdown-regex.ts";
8+
import "./mapped-string.ts";
9+
import "./jupyter.ts";
10+
import "./format.ts";
11+
import "./path.ts";
12+
import "./system.ts";
13+
import "./text.ts";
14+
import "./console.ts";
15+
import "./crypto.ts";

0 commit comments

Comments
 (0)