@vutex/vutex 0.0.0-stage → 0.0.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/bin/vutex.mjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createJiti } from "jiti";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
9
+ const { runCli } = await jiti.import(join(here, "../src/cli/main.ts"));
10
+
11
+ process.exit(await runCli(process.argv.slice(2)));
@@ -0,0 +1,18 @@
1
+ {
2
+ "cli.intro": "vutex — language-neutral document typesetting and rendering",
3
+ "cli.cmd.render": "Render a document to the configured output format",
4
+ "cli.cmd.inspect": "Inspect input format and document metadata",
5
+ "cli.cmd.package": "Manage VuTeX resource packages (fonts, templates, profiles)",
6
+ "cli.cmd.package.list": "List resource packages from manifest and lockfile",
7
+ "cli.cmd.package.add": "Add a resource package alias to vutex.toml",
8
+ "cli.cmd.package.remove": "Remove a resource package alias from vutex.toml",
9
+ "cli.cmd.package.install": "Resolve and install resource packages into the project cache",
10
+ "cli.cmd.package.update": "Refresh the resource package lockfile",
11
+ "cli.cmd.package.doctor": "Check manifest, lockfile, and package cache health",
12
+ "cli.opt.output": "Output file path, or format id when writing to stdout",
13
+ "cli.opt.profile": "Named language profile from vutex.toml",
14
+ "cli.opt.input-format": "Override input format id",
15
+ "cli.opt.json": "Emit machine-readable JSON on stdout",
16
+ "cli.opt.package-name": "Resource package name to install",
17
+ "cli.opt.package-version": "Requested package version range"
18
+ }
@@ -0,0 +1 @@
1
+ ["en-US"]
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
+ "bin": {
3
+ "vutex": "bin/vutex.mjs"
4
+ },
5
+ "dependencies": {
6
+ "@iarna/toml": "^2.2.5",
7
+ "@vmz/commander": "^0.2.0",
8
+ "jiti": "^2.4.2"
9
+ },
10
+ "description": "VuTeX: language-neutral document typesetting runtime and official CLI",
11
+ "devDependencies": {
12
+ "@types/node": "^22.13.10",
13
+ "tsx": "^4.19.3",
14
+ "typescript": "^5.8.3"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "exports": {
20
+ ".": "./src/index.ts",
21
+ "./cli": "./src/cli/main.ts",
22
+ "./node": "./src/node/index.ts"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "locales",
27
+ "readme.md",
28
+ "src"
29
+ ],
30
+ "license": "MPL-2.0",
2
31
  "name": "@vutex/vutex",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
32
+ "optionalDependencies": {
33
+ "@vutex/vutex-darwin-arm64": "0.0.1",
34
+ "@vutex/vutex-darwin-x64": "0.0.1",
35
+ "@vutex/vutex-linux-arm64": "0.0.1",
36
+ "@vutex/vutex-linux-x64": "0.0.1",
37
+ "@vutex/vutex-win32-x64": "0.0.1"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "directory": "projects/packages/vutex",
44
+ "type": "git",
45
+ "url": "git+https://github.com/notedge/VuTeX.git"
46
+ },
47
+ "scripts": {
48
+ "test": "tsx --test tests/package-manager.test.ts",
49
+ "typecheck": "tsc --noEmit"
50
+ },
51
+ "type": "module",
52
+ "version": "0.0.1"
53
+ }
package/readme.md ADDED
@@ -0,0 +1,18 @@
1
+ # @vutex/vutex
2
+
3
+ VuTeX TypeScript runtime facade and official CLI.
4
+
5
+ ```bash
6
+ vutex render sample.nd --output out.svg
7
+ vutex inspect sample.nd
8
+ vutex package doctor
9
+ ```
10
+
11
+ ```ts
12
+ import { createVuTeX } from "@vutex/vutex";
13
+
14
+ const vutex = await createVuTeX({ projectRoot: process.cwd() });
15
+ const result = await vutex.render("sample.nd", { output: "svg" });
16
+ ```
17
+
18
+ Resource packages (fonts, templates, profiles) are managed by the VuTeX package manager via `vutex.toml`, not npm.
package/src/cli/cli.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { createCli } from "@vmz/commander";
5
+
6
+ import { registerInspectCommand } from "./commands/inspect.js";
7
+ import { registerPackageCommands } from "./commands/package.js";
8
+ import { registerRenderCommand } from "./commands/render.js";
9
+
10
+ export function buildVuTeXCli() {
11
+ const localesRoot = join(dirname(fileURLToPath(import.meta.url)), "../../locales");
12
+ const cli = createCli("vutex")
13
+ .locales(localesRoot, { envKeys: ["VUTEX_LOCALE", "LOCALE", "LANG", "LC_ALL"] })
14
+ .intro("cli.intro");
15
+
16
+ registerRenderCommand(cli);
17
+ registerInspectCommand(cli);
18
+ registerPackageCommands(cli);
19
+
20
+ return cli;
21
+ }
@@ -0,0 +1,33 @@
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import { createVuTeXRuntime } from "../context.js";
4
+ import { emitJson, presentDiagnostics } from "../diagnostics.js";
5
+ import { ExitCode } from "../exit-codes.js";
6
+
7
+ export function registerInspectCommand(cli: Cli): void {
8
+ cli.command("inspect", "cli.cmd.inspect")
9
+ .option("--json", "cli.opt.json")
10
+ .action(async (options: ParsedOptions) => {
11
+ const inputPath = options._[0];
12
+ if (!inputPath || typeof inputPath !== "string") {
13
+ return ExitCode.InvalidArgs;
14
+ }
15
+
16
+ const runtime = await createVuTeXRuntime();
17
+ const result = await runtime.inspect(inputPath);
18
+
19
+ if (options.json === true) {
20
+ emitJson(result);
21
+ } else {
22
+ console.log(`${result.inputFormat}\t${result.path}`);
23
+ }
24
+
25
+ if (result.diagnostics.length > 0) {
26
+ presentDiagnostics(result.diagnostics, options.json === true);
27
+ }
28
+
29
+ return result.diagnostics.some((item) => item.severity === "error")
30
+ ? ExitCode.ActionFailed
31
+ : ExitCode.Ok;
32
+ });
33
+ }
@@ -0,0 +1,99 @@
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import { createVuTeXRuntime } from "../context.js";
4
+ import { emitHuman, emitJson } from "../diagnostics.js";
5
+ import { ExitCode } from "../exit-codes.js";
6
+
7
+ function readName(options: ParsedOptions): string | undefined {
8
+ const value = options._[0] ?? options.name;
9
+ return typeof value === "string" ? value : undefined;
10
+ }
11
+
12
+ export function registerPackageCommands(cli: Cli): void {
13
+ const group = cli.command("package", "cli.cmd.package");
14
+
15
+ group
16
+ .command("list", "cli.cmd.package.list")
17
+ .option("--json", "cli.opt.json")
18
+ .action(async (options: ParsedOptions) => {
19
+ const runtime = await createVuTeXRuntime();
20
+ const items = await runtime.packages.list();
21
+ if (options.json === true) {
22
+ emitJson({ packages: items });
23
+ } else {
24
+ for (const item of items) {
25
+ emitHuman(`${item.name}\t${item.version}\t${item.source}`);
26
+ }
27
+ }
28
+ return ExitCode.Ok;
29
+ });
30
+
31
+ group
32
+ .command("add", "cli.cmd.package.add")
33
+ .action(async (options: ParsedOptions) => {
34
+ const name = readName(options);
35
+ if (!name) {
36
+ return ExitCode.InvalidArgs;
37
+ }
38
+ const runtime = await createVuTeXRuntime();
39
+ await runtime.packages.add(name, {
40
+ version: typeof options.version === "string" ? options.version : undefined,
41
+ });
42
+ return ExitCode.Ok;
43
+ });
44
+
45
+ group
46
+ .command("remove", "cli.cmd.package.remove")
47
+ .action(async (options: ParsedOptions) => {
48
+ const name = readName(options);
49
+ if (!name) {
50
+ return ExitCode.InvalidArgs;
51
+ }
52
+ const runtime = await createVuTeXRuntime();
53
+ await runtime.packages.remove(name);
54
+ return ExitCode.Ok;
55
+ });
56
+
57
+ group
58
+ .command("install", "cli.cmd.package.install")
59
+ .option("--name <name>", "cli.opt.package-name")
60
+ .option("--version <version>", "cli.opt.package-version")
61
+ .action(async (options: ParsedOptions) => {
62
+ const runtime = await createVuTeXRuntime();
63
+ await runtime.packages.install({
64
+ name: typeof options.name === "string" ? options.name : undefined,
65
+ version: typeof options.version === "string" ? options.version : undefined,
66
+ });
67
+ return ExitCode.Ok;
68
+ });
69
+
70
+ group
71
+ .command("update", "cli.cmd.package.update")
72
+ .action(async () => {
73
+ const runtime = await createVuTeXRuntime();
74
+ await runtime.packages.update();
75
+ return ExitCode.Ok;
76
+ });
77
+
78
+ group
79
+ .command("doctor", "cli.cmd.package.doctor")
80
+ .option("--json", "cli.opt.json")
81
+ .action(async (options: ParsedOptions) => {
82
+ const runtime = await createVuTeXRuntime();
83
+ const report = await runtime.packages.doctor();
84
+ if (options.json === true) {
85
+ emitJson(report);
86
+ } else if (report.ok) {
87
+ emitHuman("VuTeX resource packages look healthy");
88
+ for (const check of report.checks) {
89
+ emitHuman(` ok ${check.id}: ${check.message}`);
90
+ }
91
+ } else {
92
+ emitHuman("VuTeX resource packages have issues");
93
+ for (const check of report.checks) {
94
+ emitHuman(` ${check.ok ? "ok" : "fail"} ${check.id}: ${check.message}`);
95
+ }
96
+ }
97
+ return report.ok ? ExitCode.Ok : ExitCode.ActionFailed;
98
+ });
99
+ }
@@ -0,0 +1,38 @@
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import { createVuTeXRuntime } from "../context.js";
4
+ import { emitJson, presentDiagnostics } from "../diagnostics.js";
5
+ import { ExitCode } from "../exit-codes.js";
6
+
7
+ export function registerRenderCommand(cli: Cli): void {
8
+ cli.command("render", "cli.cmd.render")
9
+ .option("-o, --output <file>", "cli.opt.output")
10
+ .option("--profile <name>", "cli.opt.profile")
11
+ .option("--input <format>", "cli.opt.input-format")
12
+ .option("--json", "cli.opt.json")
13
+ .action(async (options: ParsedOptions) => {
14
+ const inputPath = options._[0];
15
+ if (!inputPath || typeof inputPath !== "string") {
16
+ return ExitCode.InvalidArgs;
17
+ }
18
+
19
+ const runtime = await createVuTeXRuntime();
20
+ const result = await runtime.render(inputPath, {
21
+ output: typeof options.output === "string" ? options.output : undefined,
22
+ profile: typeof options.profile === "string" ? options.profile : undefined,
23
+ input: typeof options.input === "string" ? options.input : undefined,
24
+ });
25
+
26
+ if (options.json === true) {
27
+ emitJson(result);
28
+ } else if (result.svg) {
29
+ console.log(result.svg);
30
+ }
31
+
32
+ if (result.diagnostics.length > 0) {
33
+ presentDiagnostics(result.diagnostics, options.json === true);
34
+ }
35
+
36
+ return result.exitCode === 0 ? ExitCode.Ok : ExitCode.ActionFailed;
37
+ });
38
+ }
@@ -0,0 +1,27 @@
1
+ import { createVuTeX, type VuTeXRuntime } from "../index.js";
2
+ import { loadVuTeXNode } from "../node/load.js";
3
+
4
+ export type CliContext = {
5
+ runtime: VuTeXRuntime | null;
6
+ toolVersion: string;
7
+ };
8
+
9
+ export async function createVuTeXRuntime(projectRoot?: string): Promise<VuTeXRuntime> {
10
+ return createVuTeX({ projectRoot: projectRoot ?? process.cwd(), host: "node", backend: "native" });
11
+ }
12
+
13
+ export function loadBindings(): ReturnType<typeof loadVuTeXNode> | null {
14
+ try {
15
+ return loadVuTeXNode();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export async function createContext(projectRoot?: string): Promise<CliContext> {
22
+ const bindings = loadBindings();
23
+ return {
24
+ runtime: bindings ? await createVuTeX({ projectRoot: projectRoot ?? process.cwd() }) : null,
25
+ toolVersion: bindings?.vutexVersion() ?? "0.0.0",
26
+ };
27
+ }
@@ -0,0 +1,23 @@
1
+ import type { VuTeXDiagnostic } from "../types.js";
2
+
3
+ export function emitHuman(message: string): void {
4
+ console.error(message);
5
+ }
6
+
7
+ export function emitJson(value: unknown): void {
8
+ console.log(JSON.stringify(value, null, 2));
9
+ }
10
+
11
+ export function presentDiagnostics(
12
+ diagnostics: VuTeXDiagnostic[],
13
+ json: boolean,
14
+ ): void {
15
+ if (json) {
16
+ emitJson({ diagnostics });
17
+ return;
18
+ }
19
+ for (const item of diagnostics) {
20
+ const stage = item.stage ? `[${item.stage}] ` : "";
21
+ emitHuman(`${stage}${item.severity}: ${item.code} — ${item.message}`);
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ export const ExitCode = {
2
+ Ok: 0,
3
+ ActionFailed: 1,
4
+ InvalidArgs: 2,
5
+ } as const;
@@ -0,0 +1,5 @@
1
+ import { buildVuTeXCli } from "./cli.js";
2
+
3
+ export async function runCli(argv: string[]): Promise<number> {
4
+ return await buildVuTeXCli().parse(argv);
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { loadVuTeXNode } from "./node/load.js";
2
+ import { createVuTeXRuntime } from "./runtime.js";
3
+ import type { CreateVuTeXOptions, VuTeXRuntime } from "./types.js";
4
+
5
+ export type {
6
+ CreateVuTeXOptions,
7
+ FormulaDiagnostic,
8
+ FormulaMetrics,
9
+ FormulaRender,
10
+ FormulaRenderOptions,
11
+ InspectDocumentResult,
12
+ PackageDoctorReport,
13
+ PackageInstallOptions,
14
+ PackageRecord,
15
+ RenderDocumentOptions,
16
+ RenderDocumentResult,
17
+ VuTeXBackend,
18
+ VuTeXBindings,
19
+ VuTeXDiagnostic,
20
+ VuTeXHost,
21
+ VuTeXRuntime,
22
+ } from "./types.js";
23
+
24
+ export { loadVuTeXNode } from "./node/index.js";
25
+ export { createVuTeXRuntime } from "./runtime.js";
26
+
27
+ /** Create a VuTeX runtime for Node hosts (native binding). */
28
+ export async function createVuTeX(options: CreateVuTeXOptions = {}): Promise<VuTeXRuntime> {
29
+ const backend = options.backend ?? "auto";
30
+ const host = options.host ?? "node";
31
+ if (host !== "node" && backend !== "native") {
32
+ throw new Error("createVuTeX scaffold currently supports Node native hosts only");
33
+ }
34
+ const bindings = loadVuTeXNode();
35
+ return createVuTeXRuntime(bindings, options);
36
+ }
@@ -0,0 +1 @@
1
+ export { loadVuTeXNode } from "./load.js";
@@ -0,0 +1,50 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import type { VuTeXBindings } from "../types.js";
4
+
5
+ type NativeBinding = {
6
+ vutexVersion: () => string;
7
+ measureFormula: (source: string, optionsJson?: string) => string;
8
+ renderFormulaSvg: (source: string, optionsJson?: string) => string;
9
+ renderFormulaMathml: (source: string, optionsJson?: string) => string;
10
+ renderFormula: (source: string, optionsJson?: string) => string;
11
+ inspectDocument: (inputPath: string) => {
12
+ inputFormat: string;
13
+ path: string;
14
+ reportJson: string;
15
+ };
16
+ renderDocument: (inputPath: string, optionsJson?: string) => {
17
+ exitCode: number;
18
+ outputFormat: string;
19
+ svg?: string;
20
+ reportJson: string;
21
+ };
22
+ };
23
+
24
+ const PLATFORM_PACKAGES: Record<string, string> = {
25
+ "win32-x64": "@vutex/vutex-win32-x64",
26
+ "linux-x64": "@vutex/vutex-linux-x64",
27
+ "linux-arm64": "@vutex/vutex-linux-arm64",
28
+ "darwin-x64": "@vutex/vutex-darwin-x64",
29
+ "darwin-arm64": "@vutex/vutex-darwin-arm64",
30
+ };
31
+
32
+ /** Load the platform-specific Node-API binary from `@vutex/vutex-<platform>`. */
33
+ export function loadVuTeXNode(): VuTeXBindings {
34
+ const key = `${process.platform}-${process.arch}`;
35
+ const pkg = PLATFORM_PACKAGES[key];
36
+ if (!pkg) {
37
+ throw new Error(`Unsupported platform for VuTeX native bindings: ${key}`);
38
+ }
39
+ const require = createRequire(import.meta.url);
40
+ const binding = require(pkg).default as NativeBinding;
41
+ return {
42
+ vutexVersion: () => binding.vutexVersion(),
43
+ measureFormula: (source, optionsJson) => binding.measureFormula(source, optionsJson),
44
+ renderFormulaSvg: (source, optionsJson) => binding.renderFormulaSvg(source, optionsJson),
45
+ renderFormulaMathml: (source, optionsJson) => binding.renderFormulaMathml(source, optionsJson),
46
+ renderFormula: (source, optionsJson) => binding.renderFormula(source, optionsJson),
47
+ inspectDocument: (inputPath) => binding.inspectDocument(inputPath),
48
+ renderDocument: (inputPath, optionsJson) => binding.renderDocument(inputPath, optionsJson),
49
+ };
50
+ }
@@ -0,0 +1,35 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import TOML from "@iarna/toml";
5
+
6
+ export type LockedPackage = {
7
+ name: string;
8
+ version: string;
9
+ source: string;
10
+ contentHash: string;
11
+ dependencies: string[];
12
+ };
13
+
14
+ export type VuTeXLockfile = {
15
+ version: number;
16
+ packages: LockedPackage[];
17
+ };
18
+
19
+ export async function readLockfile(projectRoot: string): Promise<VuTeXLockfile | null> {
20
+ const path = join(projectRoot, "vutex.lock");
21
+ try {
22
+ const raw = await readFile(path, "utf8");
23
+ return TOML.parse(raw) as VuTeXLockfile;
24
+ } catch (error) {
25
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
26
+ return null;
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ export async function writeLockfile(projectRoot: string, lockfile: VuTeXLockfile): Promise<void> {
33
+ const path = join(projectRoot, "vutex.lock");
34
+ await writeFile(path, `${TOML.stringify(lockfile)}\n`, "utf8");
35
+ }
@@ -0,0 +1,119 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import TOML from "@iarna/toml";
5
+
6
+ import type { PackageDoctorReport, PackageInstallOptions, PackageRecord } from "../types.js";
7
+ import { readLockfile, writeLockfile, type LockedPackage } from "./lockfile.js";
8
+ import { readManifest } from "./manifest.js";
9
+
10
+ function hashPlaceholder(name: string, version: string): string {
11
+ return `sha256:scaffold:${name}@${version}`;
12
+ }
13
+
14
+ export class PackageManager {
15
+ constructor(private readonly projectRoot: string) {}
16
+
17
+ async list(): Promise<PackageRecord[]> {
18
+ const lock = await readLockfile(this.projectRoot);
19
+ if (!lock) {
20
+ const manifest = await readManifest(this.projectRoot);
21
+ if (!manifest?.packages) {
22
+ return [];
23
+ }
24
+ return Object.entries(manifest.packages).map(([alias, version]) => ({
25
+ name: alias,
26
+ version,
27
+ source: "manifest",
28
+ }));
29
+ }
30
+ return lock.packages.map((item) => ({
31
+ name: item.name,
32
+ version: item.version,
33
+ source: item.source,
34
+ }));
35
+ }
36
+
37
+ async add(name: string, options: PackageInstallOptions = {}): Promise<void> {
38
+ const manifest = (await readManifest(this.projectRoot)) ?? {};
39
+ const packages = { ...(manifest.packages ?? {}) };
40
+ const alias = name.includes("/") ? name.split("/").pop() ?? name : name;
41
+ packages[alias] = options.version ?? "latest";
42
+ const next = {
43
+ ...manifest,
44
+ packages,
45
+ };
46
+ await writeFile(join(this.projectRoot, "vutex.toml"), `${TOML.stringify(next)}\n`, "utf8");
47
+ }
48
+
49
+ async remove(name: string): Promise<void> {
50
+ const manifest = await readManifest(this.projectRoot);
51
+ if (!manifest?.packages) {
52
+ return;
53
+ }
54
+ const packages = { ...manifest.packages };
55
+ delete packages[name];
56
+ const next = { ...manifest, packages };
57
+ await writeFile(join(this.projectRoot, "vutex.toml"), `${TOML.stringify(next)}\n`, "utf8");
58
+ }
59
+
60
+ async install(options: PackageInstallOptions = {}): Promise<void> {
61
+ const manifest = await readManifest(this.projectRoot);
62
+ const cacheDir = join(this.projectRoot, ".vutex", "packages");
63
+ await mkdir(cacheDir, { recursive: true });
64
+
65
+ const entries: LockedPackage[] = [];
66
+ const sourcePackages = manifest?.packages ?? {};
67
+ if (options.name) {
68
+ sourcePackages[options.name] = options.version ?? "latest";
69
+ }
70
+
71
+ for (const [alias, version] of Object.entries(sourcePackages)) {
72
+ entries.push({
73
+ name: alias,
74
+ version,
75
+ source: `@vutex/${alias}`,
76
+ contentHash: hashPlaceholder(alias, version),
77
+ dependencies: [],
78
+ });
79
+ }
80
+
81
+ await writeLockfile(this.projectRoot, {
82
+ version: 1,
83
+ packages: entries,
84
+ });
85
+ }
86
+
87
+ async update(): Promise<void> {
88
+ await this.install();
89
+ }
90
+
91
+ async doctor(): Promise<PackageDoctorReport> {
92
+ const checks: PackageDoctorReport["checks"] = [];
93
+ const manifest = await readManifest(this.projectRoot);
94
+ const lock = await readLockfile(this.projectRoot);
95
+
96
+ checks.push({
97
+ id: "manifest",
98
+ ok: manifest !== null,
99
+ message: manifest ? "vutex.toml present" : "vutex.toml missing (optional for scaffold)",
100
+ });
101
+
102
+ if (manifest?.packages && Object.keys(manifest.packages).length > 0) {
103
+ checks.push({
104
+ id: "lockfile",
105
+ ok: lock !== null && lock.packages.length > 0,
106
+ message: lock ? `vutex.lock tracks ${lock.packages.length} package(s)` : "vutex.lock missing",
107
+ });
108
+ } else {
109
+ checks.push({
110
+ id: "lockfile",
111
+ ok: true,
112
+ message: "no resource packages declared",
113
+ });
114
+ }
115
+
116
+ const ok = checks.every((check) => check.ok);
117
+ return { ok, checks };
118
+ }
119
+ }
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import TOML from "@iarna/toml";
5
+
6
+ export type VuTeXManifest = {
7
+ document?: {
8
+ input?: string;
9
+ output?: string;
10
+ };
11
+ profile?: {
12
+ name?: string;
13
+ };
14
+ packages?: Record<string, string>;
15
+ };
16
+
17
+ export async function readManifest(projectRoot: string): Promise<VuTeXManifest | null> {
18
+ const path = join(projectRoot, "vutex.toml");
19
+ try {
20
+ const raw = await readFile(path, "utf8");
21
+ return TOML.parse(raw) as VuTeXManifest;
22
+ } catch (error) {
23
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
24
+ return null;
25
+ }
26
+ throw error;
27
+ }
28
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type {
2
+ CreateVuTeXOptions,
3
+ FormulaRender,
4
+ FormulaRenderOptions,
5
+ InspectDocumentOptions,
6
+ InspectDocumentResult,
7
+ RenderDocumentOptions,
8
+ RenderDocumentResult,
9
+ VuTeXBindings,
10
+ VuTeXDiagnostic,
11
+ VuTeXRuntime,
12
+ } from "./types.js";
13
+ import { PackageManager } from "./packages/manager.js";
14
+ import { readManifest } from "./packages/manifest.js";
15
+
16
+ function encodeFormulaOptions(options?: FormulaRenderOptions): string | undefined {
17
+ if (!options) {
18
+ return undefined;
19
+ }
20
+ return JSON.stringify({ displayMode: options.displayMode ?? "inline" });
21
+ }
22
+
23
+ function decodeFormulaRender(raw: string): FormulaRender {
24
+ return JSON.parse(raw) as FormulaRender;
25
+ }
26
+
27
+ function diagnosticsFromReport(reportJson: string): VuTeXDiagnostic[] {
28
+ try {
29
+ const report = JSON.parse(reportJson) as {
30
+ diagnostics?: VuTeXDiagnostic[];
31
+ };
32
+ return report.diagnostics ?? [];
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+
38
+ export function createVuTeXRuntime(
39
+ bindings: VuTeXBindings,
40
+ options: CreateVuTeXOptions = {},
41
+ ): VuTeXRuntime {
42
+ const projectRoot = options.projectRoot ?? process.cwd();
43
+ const packages = new PackageManager(projectRoot);
44
+
45
+ return {
46
+ version: () => bindings.vutexVersion(),
47
+ async render(inputPath, renderOptions = {}): Promise<RenderDocumentResult> {
48
+ const manifest = await readManifest(projectRoot);
49
+ const payload = JSON.stringify({
50
+ output: renderOptions.output ?? manifest?.document?.output ?? "svg",
51
+ profile: renderOptions.profile ?? manifest?.profile?.name,
52
+ input: renderOptions.input ?? manifest?.document?.input,
53
+ });
54
+ const response = bindings.renderDocument(inputPath, payload);
55
+ return {
56
+ exitCode: response.exitCode,
57
+ outputFormat: response.outputFormat,
58
+ svg: response.svg,
59
+ reportJson: response.reportJson,
60
+ diagnostics: diagnosticsFromReport(response.reportJson),
61
+ };
62
+ },
63
+ async inspect(inputPath, _options: InspectDocumentOptions = {}): Promise<InspectDocumentResult> {
64
+ const response = bindings.inspectDocument(inputPath);
65
+ return {
66
+ inputFormat: response.inputFormat,
67
+ path: response.path,
68
+ reportJson: response.reportJson,
69
+ diagnostics: diagnosticsFromReport(response.reportJson),
70
+ };
71
+ },
72
+ measureFormula(source, options) {
73
+ return decodeFormulaRender(bindings.measureFormula(source, encodeFormulaOptions(options)));
74
+ },
75
+ renderFormula(source, options) {
76
+ return decodeFormulaRender(bindings.renderFormula(source, encodeFormulaOptions(options)));
77
+ },
78
+ packages: {
79
+ list: () => packages.list(),
80
+ add: (name, opts) => packages.add(name, opts),
81
+ remove: (name) => packages.remove(name),
82
+ install: (opts) => packages.install(opts),
83
+ update: () => packages.update(),
84
+ doctor: () => packages.doctor(),
85
+ },
86
+ };
87
+ }
package/src/types.ts ADDED
@@ -0,0 +1,121 @@
1
+ export type VuTeXHost = "node" | "browser" | "wasm";
2
+
3
+ export type VuTeXBackend = "auto" | "native" | "wasm";
4
+
5
+ export type CreateVuTeXOptions = {
6
+ projectRoot?: string;
7
+ host?: VuTeXHost;
8
+ backend?: VuTeXBackend;
9
+ };
10
+
11
+ export type RenderDocumentOptions = {
12
+ output?: string;
13
+ profile?: string;
14
+ input?: string;
15
+ };
16
+
17
+ export type InspectDocumentOptions = {
18
+ input?: string;
19
+ };
20
+
21
+ export type FormulaRenderOptions = {
22
+ displayMode?: "inline" | "block";
23
+ };
24
+
25
+ export type FormulaDiagnostic = {
26
+ code: string;
27
+ severity: "info" | "warning" | "error";
28
+ message: string;
29
+ start?: number;
30
+ end?: number;
31
+ };
32
+
33
+ export type FormulaMetrics = {
34
+ width: number;
35
+ height: number;
36
+ depth: number;
37
+ baseline: number;
38
+ };
39
+
40
+ export type FormulaRender = {
41
+ metrics: FormulaMetrics;
42
+ svg: string;
43
+ mathml?: string;
44
+ diagnostics: FormulaDiagnostic[];
45
+ };
46
+
47
+ export type VuTeXDiagnostic = {
48
+ code: string;
49
+ severity: "info" | "warning" | "error";
50
+ message: string;
51
+ stage?: string;
52
+ path?: string;
53
+ args?: Record<string, unknown>;
54
+ cause?: string;
55
+ };
56
+
57
+ export type RenderDocumentResult = {
58
+ exitCode: number;
59
+ outputFormat: string;
60
+ svg?: string;
61
+ reportJson: string;
62
+ diagnostics: VuTeXDiagnostic[];
63
+ };
64
+
65
+ export type InspectDocumentResult = {
66
+ inputFormat: string;
67
+ path: string;
68
+ reportJson: string;
69
+ diagnostics: VuTeXDiagnostic[];
70
+ };
71
+
72
+ export type PackageInstallOptions = {
73
+ name?: string;
74
+ version?: string;
75
+ };
76
+
77
+ export type PackageRecord = {
78
+ name: string;
79
+ version: string;
80
+ source: string;
81
+ };
82
+
83
+ export type PackageDoctorReport = {
84
+ ok: boolean;
85
+ checks: Array<{ id: string; ok: boolean; message: string }>;
86
+ };
87
+
88
+ export type VuTeXBindings = {
89
+ vutexVersion: () => string;
90
+ measureFormula: (source: string, optionsJson?: string) => string;
91
+ renderFormulaSvg: (source: string, optionsJson?: string) => string;
92
+ renderFormulaMathml: (source: string, optionsJson?: string) => string;
93
+ renderFormula: (source: string, optionsJson?: string) => string;
94
+ inspectDocument: (inputPath: string) => {
95
+ inputFormat: string;
96
+ path: string;
97
+ reportJson: string;
98
+ };
99
+ renderDocument: (inputPath: string, optionsJson?: string) => {
100
+ exitCode: number;
101
+ outputFormat: string;
102
+ svg?: string;
103
+ reportJson: string;
104
+ };
105
+ };
106
+
107
+ export type VuTeXRuntime = {
108
+ version: () => string;
109
+ render: (inputPath: string, options?: RenderDocumentOptions) => Promise<RenderDocumentResult>;
110
+ inspect: (inputPath: string, options?: InspectDocumentOptions) => Promise<InspectDocumentResult>;
111
+ measureFormula: (source: string, options?: FormulaRenderOptions) => FormulaRender;
112
+ renderFormula: (source: string, options?: FormulaRenderOptions) => FormulaRender;
113
+ packages: {
114
+ list: () => Promise<PackageRecord[]>;
115
+ add: (name: string, options?: PackageInstallOptions) => Promise<void>;
116
+ remove: (name: string) => Promise<void>;
117
+ install: (options?: PackageInstallOptions) => Promise<void>;
118
+ update: () => Promise<void>;
119
+ doctor: () => Promise<PackageDoctorReport>;
120
+ };
121
+ };
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.