@tstdl/base 0.93.67 → 0.93.69

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,23 @@
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
+ * ## WARNING
13
+ * **This function should not be used with untrusted LaTeX source, as it can lead to arbitrary code execution on the system.**
14
+ *
15
+ * Requires latexmk and LuaTeX to be installed on the system.
16
+ *
17
+ * **Minimal recommendation:**
18
+ * - **Arch Linux:** texlive-binextra texlive-luatex texlive-latexrecommended texlive-fontsrecommended
19
+ * @param source The LaTeX source code to render
20
+ * @param options Rendering options
21
+ * @returns A TemporaryFile representing the generated PDF
22
+ */
23
+ export declare function renderLatex(source: string, options?: LatexRenderOptions): Promise<TemporaryFile>;
@@ -0,0 +1,102 @@
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 { spawnWaitCommand, spawnWaitReadCommand } from '../process/spawn.js';
55
+ import { withTimeout } from '../utils/timing.js';
56
+ import { tryIgnoreAsync } from '../utils/try-ignore.js';
57
+ const engineMapping = {
58
+ pdflatex: '-pdflatex',
59
+ lualatex: '-pdflua',
60
+ xelatex: '-pdfxe',
61
+ };
62
+ /**
63
+ * Renders LaTeX source code to a PDF file.
64
+ *
65
+ * ## WARNING
66
+ * **This function should not be used with untrusted LaTeX source, as it can lead to arbitrary code execution on the system.**
67
+ *
68
+ * Requires latexmk and LuaTeX to be installed on the system.
69
+ *
70
+ * **Minimal recommendation:**
71
+ * - **Arch Linux:** texlive-binextra texlive-luatex texlive-latexrecommended texlive-fontsrecommended
72
+ * @param source The LaTeX source code to render
73
+ * @param options Rendering options
74
+ * @returns A TemporaryFile representing the generated PDF
75
+ */
76
+ export async function renderLatex(source, options) {
77
+ const env_1 = { stack: [], error: void 0, hasError: false };
78
+ try {
79
+ const latexFile = __addDisposableResource(env_1, await TemporaryFile.from(source, '.tex'), true);
80
+ const engineFlag = engineMapping[options?.engine ?? 'lualatex'];
81
+ const { code, output, error } = await spawnWaitReadCommand('string', 'latexmk', ['-interaction=nonstopmode', engineFlag, '-cd', latexFile.path], { throwOnNonZeroExitCode: false });
82
+ await tryIgnoreAsync(async () => await withTimeout(1000, spawnWaitCommand('latexmk', ['-c', '-cd', engineFlag, latexFile.path])));
83
+ if (code !== 0) {
84
+ throw new Error(`
85
+ LaTeX compilation failed with exit code ${code}.\n
86
+ File: ${latexFile.path}\n
87
+ Output:\n${output}\n
88
+ Error Output:\n${error}
89
+ `.trim());
90
+ }
91
+ return TemporaryFile.adopt(`${latexFile.path.slice(0, -4)}.pdf`);
92
+ }
93
+ catch (e_1) {
94
+ env_1.error = e_1;
95
+ env_1.hasError = true;
96
+ }
97
+ finally {
98
+ const result_1 = __disposeResources(env_1);
99
+ if (result_1)
100
+ await result_1;
101
+ }
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.67",
3
+ "version": "0.93.69",
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,22 +1,43 @@
1
1
  import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
- type WaitOptions = {
2
+ import type { Record } from '../types/types.js';
3
+ type WaitReadResultFormat = 'string' | 'binary';
4
+ type WaitReadResultFormatType<T extends WaitReadResultFormat> = T extends 'string' ? string : Uint8Array<ArrayBuffer>;
5
+ export type WaitOptions = {
3
6
  throwOnNonZeroExitCode?: boolean;
4
7
  };
5
- type ProcessResult = {
8
+ export type WaitResult = {
6
9
  code: number | null;
7
10
  signal: string | null;
8
11
  };
12
+ export type WaitReadResult<Format extends WaitReadResultFormat> = WaitResult & {
13
+ output: WaitReadResultFormatType<Format>;
14
+ error: WaitReadResultFormatType<Format>;
15
+ };
16
+ export type SpawnOptions = {
17
+ arguments?: string[];
18
+ workingDirectory?: string;
19
+ environment?: Record<string, string>;
20
+ };
9
21
  export type SpawnCommandResult = TransformStream<Uint8Array, Uint8Array> & {
10
22
  process: ChildProcessWithoutNullStreams;
11
23
  stderr: ReadableStream<Uint8Array>;
12
24
  write(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): Promise<void>;
13
- autoWrite(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): void;
25
+ writeInBackground(chunk: ReadableStream<Uint8Array> | Uint8Array | string, options?: StreamPipeOptions): void;
14
26
  readOutputBytes(): Promise<Uint8Array>;
15
27
  readOutput(): Promise<string>;
16
28
  readErrorBytes(): Promise<Uint8Array>;
17
29
  readError(): Promise<string>;
18
30
  handleNonZeroExitCode(): void;
19
- wait(options?: WaitOptions): Promise<ProcessResult>;
31
+ wait(options?: WaitOptions): Promise<WaitResult>;
32
+ waitRead<F extends WaitReadResultFormat>(format: F, options?: WaitOptions): Promise<WaitReadResult<F>>;
20
33
  };
21
- export declare function spawnCommand(command: string, args?: string[]): Promise<SpawnCommandResult>;
34
+ /** spwans a command and waits for it to complete */
35
+ export declare function spawnWaitCommand(command: string, args?: string[], options?: SpawnOptions & WaitOptions): Promise<WaitResult>;
36
+ export declare function spawnWaitCommand(command: string, options?: SpawnOptions & WaitOptions): Promise<WaitResult>;
37
+ /** spwans a command, waits for it to complete and reads its output */
38
+ export declare function spawnWaitReadCommand<F extends WaitReadResultFormat>(format: F, command: string, args?: string[], options?: SpawnOptions & WaitOptions): Promise<WaitReadResult<F>>;
39
+ export declare function spawnWaitReadCommand<F extends WaitReadResultFormat>(format: F, command: string, options?: SpawnOptions & WaitOptions): Promise<WaitReadResult<F>>;
40
+ /** Spawns a command as a child process. */
41
+ export declare function spawnCommand(command: string, args?: string[], options?: SpawnOptions): Promise<SpawnCommandResult>;
42
+ export declare function spawnCommand(command: string, options?: SpawnOptions): Promise<SpawnCommandResult>;
22
43
  export {};
package/process/spawn.js CHANGED
@@ -3,11 +3,22 @@ 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 spawnWaitReadCommand(format, command, argsOrOptions, optionsOrNothing) {
13
+ const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [undefined, argsOrOptions];
14
+ const process = await spawnCommand(command, args, options);
15
+ return await process.waitRead(format, { throwOnNonZeroExitCode: options?.throwOnNonZeroExitCode });
16
+ }
17
+ export async function spawnCommand(command, argsOrOptions, optionsOrNothing) {
8
18
  const { spawn } = await dynamicImport('node:child_process');
9
19
  const { Readable, Writable } = await dynamicImport('node:stream');
10
- const process = spawn(command, args, { stdio: 'pipe' });
20
+ const [args, options] = isArray(argsOrOptions) ? [argsOrOptions, optionsOrNothing] : [undefined, argsOrOptions];
21
+ const process = spawn(command, args, { stdio: 'pipe', cwd: options?.workingDirectory, env: options?.environment });
11
22
  await Promise.race([
12
23
  new Promise((resolve) => process.on('spawn', resolve)),
13
24
  new Promise((_, reject) => process.on('error', reject)),
@@ -26,7 +37,8 @@ export async function spawnCommand(command, args) {
26
37
  await toReadableStream(data).pipeThrough(encodeUtf8Stream()).pipeTo(writable, options);
27
38
  }
28
39
  }
29
- function autoWrite(data, options) {
40
+ /** Writes data in the background. Must be used with care because of potential unhandled errors */
41
+ function writeInBackground(data, options) {
30
42
  write(data, options).catch((error) => {
31
43
  readable.cancel(error).catch(() => { });
32
44
  writable.abort(error).catch(() => { });
@@ -74,18 +86,31 @@ export async function spawnCommand(command, args) {
74
86
  }
75
87
  return result;
76
88
  }
89
+ async function waitRead(format, { throwOnNonZeroExitCode = true } = {}) {
90
+ const [result, output, error] = await Promise.all([
91
+ wait({ throwOnNonZeroExitCode }),
92
+ (format === 'string') ? readOutput() : readOutputBytes(),
93
+ (format === 'string') ? readError() : readErrorBytes(),
94
+ ]);
95
+ return {
96
+ ...result,
97
+ output: output,
98
+ error: error,
99
+ };
100
+ }
77
101
  return {
78
102
  process,
79
103
  readable,
80
104
  writable,
81
105
  stderr,
82
106
  write,
83
- autoWrite,
107
+ writeInBackground,
84
108
  readOutputBytes,
85
109
  readOutput,
86
110
  readErrorBytes,
87
111
  readError,
88
112
  handleNonZeroExitCode: () => void handleNonZeroExitCode(),
89
113
  wait,
114
+ waitRead,
90
115
  };
91
116
  }
@@ -1,9 +1,9 @@
1
1
  import type { Logger } from '../logger/logger.js';
2
- export declare function tryIgnore<R>(fn: () => R): R;
2
+ export declare function tryIgnore<R>(fn: () => R): R | undefined;
3
3
  export declare function tryIgnore<R, F>(fn: () => R, fallback: F): R | F;
4
- export declare function tryIgnoreAsync<R>(fn: () => Promise<R>): Promise<R>;
4
+ export declare function tryIgnoreAsync<R>(fn: () => Promise<R>): Promise<R | undefined>;
5
5
  export declare function tryIgnoreAsync<R, F>(fn: () => Promise<R>, fallback: F): Promise<F>;
6
- export declare function tryIgnoreLog<R>(logger: Logger, fn: () => R): R;
6
+ export declare function tryIgnoreLog<R>(logger: Logger, fn: () => R): R | undefined;
7
7
  export declare function tryIgnoreLog<R, F>(logger: Logger, fn: () => R, fallback: F): R | F;
8
- export declare function tryIgnoreLogAsync<R>(logger: Logger, fn: () => Promise<R>): Promise<R>;
8
+ export declare function tryIgnoreLogAsync<R>(logger: Logger, fn: () => Promise<R>): Promise<R | undefined>;
9
9
  export declare function tryIgnoreLogAsync<R, F>(logger: Logger, fn: () => Promise<R>, fallback: F): Promise<F>;