@tstdl/base 0.93.67 → 0.93.68

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,17 +1,28 @@
1
+ /** Represents a temporary file on the filesystem which is automatically deleted on disposal. */
1
2
  export declare class TemporaryFile implements AsyncDisposable {
2
3
  #private;
3
4
  get path(): string;
4
- static create(): TemporaryFile;
5
+ static create(extension?: string): TemporaryFile;
5
6
  /**
6
7
  * Use an existing file as a temporary file which gets deleted on disposal.
7
8
  * @param path path to adopt
8
9
  */
9
10
  static adopt(path: string): TemporaryFile;
10
- static from(content: string | Uint8Array | ReadableStream<Uint8Array>): Promise<TemporaryFile>;
11
+ static from(content: string | Uint8Array | ReadableStream<Uint8Array>, extension?: string): Promise<TemporaryFile>;
12
+ /**
13
+ * Prevents the temporary file from being deleted on disposal.
14
+ */
15
+ keep(): void;
11
16
  read(): Promise<Uint8Array>;
12
17
  readText(): Promise<string>;
13
18
  readStream(): ReadableStream<Uint8Array>;
14
19
  write(content: string | Uint8Array | ReadableStream<Uint8Array>): Promise<void>;
20
+ /**
21
+ * Moves the file to a new location.
22
+ * @param path The new path for the file.
23
+ * @param keep If true, prevents the file from being deleted on disposal.
24
+ */
25
+ moveTo(path: string, keep?: boolean): Promise<void>;
15
26
  delete(): Promise<void>;
16
27
  size(): Promise<number>;
17
28
  [Symbol.asyncDispose](): Promise<void>;
@@ -1,14 +1,21 @@
1
1
  import { createReadStream } from 'node:fs';
2
- import { readFile, stat, unlink, writeFile } from 'node:fs/promises';
2
+ import { readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { Readable } from 'node:stream';
5
+ import { isDefined } from '../../utils/type-guards.js';
6
+ /** Represents a temporary file on the filesystem which is automatically deleted on disposal. */
5
7
  export class TemporaryFile {
6
8
  #path = `${tmpdir()}/${crypto.randomUUID()}`;
9
+ #keep = false;
7
10
  get path() {
8
11
  return this.#path;
9
12
  }
10
- static create() {
11
- return new TemporaryFile();
13
+ static create(extension) {
14
+ const file = new TemporaryFile();
15
+ if (isDefined(extension)) {
16
+ file.#path += extension.startsWith('.') ? extension : `.${extension}`;
17
+ }
18
+ return file;
12
19
  }
13
20
  /**
14
21
  * Use an existing file as a temporary file which gets deleted on disposal.
@@ -19,16 +26,22 @@ export class TemporaryFile {
19
26
  file.#path = path;
20
27
  return file;
21
28
  }
22
- static async from(content) {
23
- const file = new TemporaryFile();
29
+ static async from(content, extension) {
30
+ const file = TemporaryFile.create(extension);
24
31
  await file.write(content);
25
32
  return file;
26
33
  }
34
+ /**
35
+ * Prevents the temporary file from being deleted on disposal.
36
+ */
37
+ keep() {
38
+ this.#keep = true;
39
+ }
27
40
  async read() {
28
- return readFile(this.#path);
41
+ return await readFile(this.#path);
29
42
  }
30
43
  async readText() {
31
- return readFile(this.#path, { encoding: 'utf8' });
44
+ return await readFile(this.#path, { encoding: 'utf8' });
32
45
  }
33
46
  readStream() {
34
47
  const stream = createReadStream(this.#path);
@@ -37,6 +50,18 @@ export class TemporaryFile {
37
50
  async write(content) {
38
51
  await writeFile(this.#path, content);
39
52
  }
53
+ /**
54
+ * Moves the file to a new location.
55
+ * @param path The new path for the file.
56
+ * @param keep If true, prevents the file from being deleted on disposal.
57
+ */
58
+ async moveTo(path, keep) {
59
+ await rename(this.#path, path);
60
+ this.#path = path;
61
+ if (keep) {
62
+ this.keep();
63
+ }
64
+ }
40
65
  async delete() {
41
66
  await unlink(this.#path);
42
67
  }
@@ -45,6 +70,9 @@ export class TemporaryFile {
45
70
  return result.size;
46
71
  }
47
72
  async [Symbol.asyncDispose]() {
73
+ if (this.#keep) {
74
+ return;
75
+ }
48
76
  try {
49
77
  await this.delete();
50
78
  }
@@ -0,0 +1 @@
1
+ export * from './render.js';
package/latex/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './render.js';
@@ -0,0 +1,19 @@
1
+ import { TemporaryFile } from '../file/server/temporary-file.js';
2
+ export type LatexRenderOptions = {
3
+ /**
4
+ * The LaTeX engine to use for rendering the document.
5
+ * @default 'lualatex'
6
+ */
7
+ engine?: 'pdflatex' | 'lualatex' | 'xelatex';
8
+ };
9
+ /**
10
+ * Renders LaTeX source code to a PDF file.
11
+ *
12
+ * Requires latexmk and LuaTeX to be installed on the system.
13
+ *
14
+ * **Minimal recommendation:**
15
+ * - **Arch Linux:** texlive-binextra texlive-luatex texlive-latexrecommended texlive-fontsrecommended
16
+ * @param source The LaTeX source code to render
17
+ * @returns A TemporaryFile representing the generated PDF
18
+ */
19
+ export declare function renderLatex(source: string, options?: LatexRenderOptions): Promise<TemporaryFile>;
@@ -0,0 +1,95 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { TemporaryFile } from '../file/server/temporary-file.js';
54
+ import { spawnCommand, spawnWaitCommand } from '../process/spawn.js';
55
+ import { tryIgnoreAsync } from '../utils/try-ignore.js';
56
+ const engineMapping = {
57
+ pdflatex: '-pdflatex',
58
+ lualatex: '-pdflua',
59
+ xelatex: '-pdfxe',
60
+ };
61
+ /**
62
+ * Renders LaTeX source code to a PDF file.
63
+ *
64
+ * Requires latexmk and LuaTeX to be installed on the system.
65
+ *
66
+ * **Minimal recommendation:**
67
+ * - **Arch Linux:** texlive-binextra texlive-luatex texlive-latexrecommended texlive-fontsrecommended
68
+ * @param source The LaTeX source code to render
69
+ * @returns A TemporaryFile representing the generated PDF
70
+ */
71
+ export async function renderLatex(source, options) {
72
+ const env_1 = { stack: [], error: void 0, hasError: false };
73
+ try {
74
+ const latexFile = __addDisposableResource(env_1, await TemporaryFile.from(source, '.tex'), true);
75
+ const engineFlag = engineMapping[options?.engine ?? 'lualatex'];
76
+ const process = await spawnCommand('latexmk', ['-interaction=nonstopmode', engineFlag, '-cd', latexFile.path]);
77
+ console.log(latexFile.path);
78
+ const { code } = await process.wait();
79
+ if (code != 0) {
80
+ const [out, err] = await Promise.all([process.readOutput(), process.readError()]);
81
+ throw new Error(`LaTeX compilation failed with exit code ${code}. Output:\n${out}\nError Output:\n${err}`);
82
+ }
83
+ await tryIgnoreAsync(async () => await spawnWaitCommand('latexmk', ['-interaction=nonstopmode', '-pdflua', '-cd', '-c', latexFile.path]));
84
+ return TemporaryFile.adopt(`${latexFile.path.slice(0, -4)}.pdf`);
85
+ }
86
+ catch (e_1) {
87
+ env_1.error = e_1;
88
+ env_1.hasError = true;
89
+ }
90
+ finally {
91
+ const result_1 = __disposeResources(env_1);
92
+ if (result_1)
93
+ await result_1;
94
+ }
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.67",
3
+ "version": "0.93.68",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -73,6 +73,7 @@
73
73
  "./jsx": "./jsx/index.js",
74
74
  "./key-value-store": "./key-value-store/index.js",
75
75
  "./key-value-store/postgres": "./key-value-store/postgres/index.js",
76
+ "./latex": "./latex/index.js",
76
77
  "./lock": "./lock/index.js",
77
78
  "./lock/postgres": "./lock/postgres/index.js",
78
79
  "./lock/web": "./lock/web/index.js",
package/pdf/utils.js CHANGED
@@ -147,7 +147,7 @@ export async function pdfToImage(file, page, size, format) {
147
147
  const process = await spawnCommand('pdftocairo', ['-f', String(page), '-l', String(page), '-scale-to', String(size), '-singlefile', `-${format}`, path, '-']);
148
148
  process.handleNonZeroExitCode();
149
149
  if (isNotString(file)) {
150
- process.autoWrite(file);
150
+ process.writeInBackground(file);
151
151
  }
152
152
  return await process.readOutputBytes();
153
153
  }
@@ -1,16 +1,22 @@
1
1
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- type WaitOptions = {
2
+ import type { Record } from '../types/types.js';
3
+ export type WaitOptions = {
3
4
  throwOnNonZeroExitCode?: boolean;
4
5
  };
5
- type ProcessResult = {
6
+ export type ProcessResult = {
6
7
  code: number | null;
7
8
  signal: string | null;
8
9
  };
10
+ export type SpawnOptions = {
11
+ arguments?: string[];
12
+ workingDirectory?: string;
13
+ environment?: Record<string, string>;
14
+ };
9
15
  export type SpawnCommandResult = TransformStream<Uint8Array, Uint8Array> & {
10
16
  process: ChildProcessWithoutNullStreams;
11
17
  stderr: ReadableStream<Uint8Array>;
12
18
  write(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): Promise<void>;
13
- autoWrite(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): void;
19
+ writeInBackground(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): void;
14
20
  readOutputBytes(): Promise<Uint8Array>;
15
21
  readOutput(): Promise<string>;
16
22
  readErrorBytes(): Promise<Uint8Array>;
@@ -18,5 +24,9 @@ export type SpawnCommandResult = TransformStream<Uint8Array, Uint8Array> & {
18
24
  handleNonZeroExitCode(): void;
19
25
  wait(options?: WaitOptions): Promise<ProcessResult>;
20
26
  };
21
- export declare function spawnCommand(command: string, args?: string[]): Promise<SpawnCommandResult>;
22
- export {};
27
+ /** spwans a command and waits for it to complete */
28
+ export declare function spawnWaitCommand(command: string, args?: string[], options?: SpawnOptions & WaitOptions): Promise<ProcessResult>;
29
+ export declare function spawnWaitCommand(command: string, options?: SpawnOptions & WaitOptions): Promise<ProcessResult>;
30
+ /** Spawns a command as a child process. */
31
+ export declare function spawnCommand(command: string, args?: string[], options?: SpawnOptions): Promise<SpawnCommandResult>;
32
+ export declare function spawnCommand(command: string, options?: SpawnOptions): Promise<SpawnCommandResult>;
package/process/spawn.js CHANGED
@@ -3,11 +3,17 @@ import { LazyPromise } from '../promise/lazy-promise.js';
3
3
  import { decodeTextStream, encodeUtf8Stream } from '../utils/encoding.js';
4
4
  import { readBinaryStream, readTextStream } from '../utils/stream/stream-reader.js';
5
5
  import { toReadableStream } from '../utils/stream/to-readable-stream.js';
6
- import { assertNotNullOrUndefinedPass, isReadableStream, isString, isUint8Array } from '../utils/type-guards.js';
7
- export async function spawnCommand(command, args) {
6
+ import { assertNotNullOrUndefinedPass, isArray, isReadableStream, isString, isUint8Array } from '../utils/type-guards.js';
7
+ export async function spawnWaitCommand(command, argsOrOptions, optionsOrNothing) {
8
+ const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [undefined, argsOrOptions];
9
+ const process = await spawnCommand(command, args, options);
10
+ return await process.wait({ throwOnNonZeroExitCode: options?.throwOnNonZeroExitCode });
11
+ }
12
+ export async function spawnCommand(command, argsOrOptions, optionsOrNothing) {
8
13
  const { spawn } = await dynamicImport('node:child_process');
9
14
  const { Readable, Writable } = await dynamicImport('node:stream');
10
- const process = spawn(command, args, { stdio: 'pipe' });
15
+ const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [undefined, argsOrOptions];
16
+ const process = spawn(command, args, { stdio: 'pipe', cwd: options?.workingDirectory, env: options?.environment });
11
17
  await Promise.race([
12
18
  new Promise((resolve) => process.on('spawn', resolve)),
13
19
  new Promise((_, reject) => process.on('error', reject)),
@@ -26,7 +32,8 @@ export async function spawnCommand(command, args) {
26
32
  await toReadableStream(data).pipeThrough(encodeUtf8Stream()).pipeTo(writable, options);
27
33
  }
28
34
  }
29
- function autoWrite(data, options) {
35
+ /** Writes data in the background. Must be used with care because of potential unhandled errors */
36
+ function writeInBackground(data, options) {
30
37
  write(data, options).catch((error) => {
31
38
  readable.cancel(error).catch(() => { });
32
39
  writable.abort(error).catch(() => { });
@@ -80,7 +87,7 @@ export async function spawnCommand(command, args) {
80
87
  writable,
81
88
  stderr,
82
89
  write,
83
- autoWrite,
90
+ writeInBackground,
84
91
  readOutputBytes,
85
92
  readOutput,
86
93
  readErrorBytes,