@vertesia/memory 0.43.0

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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +421 -0
  3. package/lib/cjs/Builder.js +186 -0
  4. package/lib/cjs/Builder.js.map +1 -0
  5. package/lib/cjs/ContentObject.js +114 -0
  6. package/lib/cjs/ContentObject.js.map +1 -0
  7. package/lib/cjs/ContentSource.js +82 -0
  8. package/lib/cjs/ContentSource.js.map +1 -0
  9. package/lib/cjs/MemoryPack.js +228 -0
  10. package/lib/cjs/MemoryPack.js.map +1 -0
  11. package/lib/cjs/MemoryPackBuilder.js +47 -0
  12. package/lib/cjs/MemoryPackBuilder.js.map +1 -0
  13. package/lib/cjs/commands/copy.js +53 -0
  14. package/lib/cjs/commands/copy.js.map +1 -0
  15. package/lib/cjs/commands/exec.js +82 -0
  16. package/lib/cjs/commands/exec.js.map +1 -0
  17. package/lib/cjs/index.js +28 -0
  18. package/lib/cjs/index.js.map +1 -0
  19. package/lib/cjs/package.json +3 -0
  20. package/lib/cjs/utils/cmdline.js +90 -0
  21. package/lib/cjs/utils/cmdline.js.map +1 -0
  22. package/lib/cjs/utils/rewrite.js +166 -0
  23. package/lib/cjs/utils/rewrite.js.map +1 -0
  24. package/lib/cjs/utils/stream.js +27 -0
  25. package/lib/cjs/utils/stream.js.map +1 -0
  26. package/lib/cjs/utils/tar.js +185 -0
  27. package/lib/cjs/utils/tar.js.map +1 -0
  28. package/lib/esm/Builder.js +178 -0
  29. package/lib/esm/Builder.js.map +1 -0
  30. package/lib/esm/ContentObject.js +103 -0
  31. package/lib/esm/ContentObject.js.map +1 -0
  32. package/lib/esm/ContentSource.js +75 -0
  33. package/lib/esm/ContentSource.js.map +1 -0
  34. package/lib/esm/MemoryPack.js +218 -0
  35. package/lib/esm/MemoryPack.js.map +1 -0
  36. package/lib/esm/MemoryPackBuilder.js +43 -0
  37. package/lib/esm/MemoryPackBuilder.js.map +1 -0
  38. package/lib/esm/commands/copy.js +50 -0
  39. package/lib/esm/commands/copy.js.map +1 -0
  40. package/lib/esm/commands/exec.js +75 -0
  41. package/lib/esm/commands/exec.js.map +1 -0
  42. package/lib/esm/index.js +7 -0
  43. package/lib/esm/index.js.map +1 -0
  44. package/lib/esm/utils/cmdline.js +86 -0
  45. package/lib/esm/utils/cmdline.js.map +1 -0
  46. package/lib/esm/utils/rewrite.js +161 -0
  47. package/lib/esm/utils/rewrite.js.map +1 -0
  48. package/lib/esm/utils/stream.js +23 -0
  49. package/lib/esm/utils/stream.js.map +1 -0
  50. package/lib/esm/utils/tar.js +175 -0
  51. package/lib/esm/utils/tar.js.map +1 -0
  52. package/lib/tsconfig.tsbuildinfo +1 -0
  53. package/lib/types/Builder.d.ts +72 -0
  54. package/lib/types/Builder.d.ts.map +1 -0
  55. package/lib/types/ContentObject.d.ts +43 -0
  56. package/lib/types/ContentObject.d.ts.map +1 -0
  57. package/lib/types/ContentSource.d.ts +32 -0
  58. package/lib/types/ContentSource.d.ts.map +1 -0
  59. package/lib/types/MemoryPack.d.ts +46 -0
  60. package/lib/types/MemoryPack.d.ts.map +1 -0
  61. package/lib/types/MemoryPackBuilder.d.ts +18 -0
  62. package/lib/types/MemoryPackBuilder.d.ts.map +1 -0
  63. package/lib/types/commands/copy.d.ts +8 -0
  64. package/lib/types/commands/copy.d.ts.map +1 -0
  65. package/lib/types/commands/exec.d.ts +7 -0
  66. package/lib/types/commands/exec.d.ts.map +1 -0
  67. package/lib/types/index.d.ts +14 -0
  68. package/lib/types/index.d.ts.map +1 -0
  69. package/lib/types/utils/cmdline.d.ts +10 -0
  70. package/lib/types/utils/cmdline.d.ts.map +1 -0
  71. package/lib/types/utils/rewrite.d.ts +38 -0
  72. package/lib/types/utils/rewrite.d.ts.map +1 -0
  73. package/lib/types/utils/stream.d.ts +9 -0
  74. package/lib/types/utils/stream.d.ts.map +1 -0
  75. package/lib/types/utils/tar.d.ts +40 -0
  76. package/lib/types/utils/tar.d.ts.map +1 -0
  77. package/package.json +40 -0
  78. package/src/Builder.ts +239 -0
  79. package/src/ContentObject.ts +114 -0
  80. package/src/ContentSource.ts +88 -0
  81. package/src/MemoryPack.ts +233 -0
  82. package/src/MemoryPackBuilder.ts +55 -0
  83. package/src/builder.test.ts +214 -0
  84. package/src/commands/copy.ts +53 -0
  85. package/src/commands/exec.test.ts +22 -0
  86. package/src/commands/exec.ts +83 -0
  87. package/src/index.ts +14 -0
  88. package/src/utils/cmdline.test.ts +32 -0
  89. package/src/utils/cmdline.ts +92 -0
  90. package/src/utils/rewrite.test.ts +65 -0
  91. package/src/utils/rewrite.ts +167 -0
  92. package/src/utils/stream.test.ts +13 -0
  93. package/src/utils/stream.ts +27 -0
  94. package/src/utils/tar.test.ts +48 -0
  95. package/src/utils/tar.ts +203 -0
@@ -0,0 +1,53 @@
1
+ import { Builder } from "../Builder.js";
2
+ import { DocxObject, MediaObject, MediaOptions, PdfObject } from "../ContentObject.js";
3
+ import { AbstractContentSource, ContentSource, FileSource, SourceSpec } from "../ContentSource.js";
4
+ import { createPathRewrite, PathMapperFn } from "../utils/rewrite.js";
5
+
6
+ function rewritePath(source: ContentSource, index: number, mapper: PathMapperFn) {
7
+ const path = source instanceof FileSource ? source.file : '';
8
+ return mapper(path, index);
9
+ }
10
+
11
+ export interface CopyOptions {
12
+ media?: MediaOptions;
13
+ extractText?: boolean | string;
14
+ }
15
+ export function copy(builder: Builder, source: SourceSpec, toPath: string, options: CopyOptions = {}) {
16
+ const resolved = AbstractContentSource.resolve(source);
17
+ const mapperFn = createPathRewrite(toPath);
18
+ if (Array.isArray(resolved)) {
19
+ for (let i = 0, l = resolved.length; i < l; i++) {
20
+ const cs = resolved[i];
21
+ copyOne(builder, cs, rewritePath(cs, i, mapperFn), options);
22
+ }
23
+ } else {
24
+ return copyOne(builder, resolved, rewritePath(resolved, 0, mapperFn), options);
25
+ }
26
+ }
27
+
28
+ function copyOne(builder: Builder, source: ContentSource, toPath: string, options: CopyOptions) {
29
+ if (options.media) {
30
+ new MediaObject(builder, source, options.media).copyTo(toPath);
31
+ } else if (options.extractText) {
32
+ let type = options.extractText;
33
+ if (typeof type === "boolean") {
34
+ if (source instanceof FileSource) {
35
+ type = source.extname ? source.extname.slice(1) : source.extname;
36
+ } else {
37
+ throw new Error("source type for extractText must be spefcified");
38
+ }
39
+ }
40
+ switch (type) {
41
+ case "pdf":
42
+ new PdfObject(builder, source).copyTo(toPath);
43
+ break;
44
+ case "docx":
45
+ new DocxObject(builder, source).copyTo(toPath);
46
+ break;
47
+ default:
48
+ throw new Error("Unsupported extractText type: " + type);
49
+ }
50
+ } else {
51
+ builder.addEntry(toPath, source);
52
+ }
53
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { exec } from "./exec";
3
+ import fs from "fs";;
4
+
5
+ describe("exec", () => {
6
+ test("test pipe", async () => {
7
+ const output = await exec('echo "hello" | wc -c', { quiet: true });
8
+ expect(output).toBeDefined();
9
+ expect(output!.trim()).toBe("6");
10
+ });
11
+ test("test pipe with redirection", async () => {
12
+ const name = "test-" + Date.now().toString() + ".txt";
13
+ const output = await exec(`echo "hello" | wc -c > ${name}`, { quiet: true });
14
+ try {
15
+ expect(output).toBeUndefined();
16
+ const content = fs.readFileSync(name, 'utf-8').trim();
17
+ expect(content).toBe("6");
18
+ } finally {
19
+ fs.unlinkSync(name);
20
+ }
21
+ });
22
+ });
@@ -0,0 +1,83 @@
1
+ import { ChildProcess, spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import { Stream, Writable } from 'stream';
4
+ import { Command, splitPipeCommands } from '../utils/cmdline.js';
5
+ import { BufferWritableStream } from '../utils/stream.js';
6
+
7
+ export interface ExecOptions {
8
+ quiet?: boolean;
9
+ }
10
+
11
+ export async function exec(commandLine: string, options: ExecOptions = {}): Promise<string | undefined> {
12
+ const verbose = !options.quiet;
13
+ commandLine = commandLine.trim();
14
+ const { commands, out } = splitPipeCommands(commandLine);
15
+ if (!commands.length) {
16
+ throw new Error('Invalid command line. No command: ' + commandLine);
17
+ }
18
+
19
+ let outStream: Writable;
20
+ if (out) {
21
+ outStream = fs.createWriteStream(out, { flags: 'w' });
22
+ } else {
23
+ outStream = new BufferWritableStream();
24
+ }
25
+ const pipePromise = executePipe(commands, outStream, verbose);
26
+ const outPromise = new Promise((resolve, reject) => {
27
+ outStream.on('finish', () => {
28
+ resolve(undefined);
29
+ });
30
+ outStream.on('error', (err: Error) => {
31
+ reject(err);
32
+ outStream.destroy();
33
+ });
34
+ });
35
+
36
+ const [status] = await Promise.all([pipePromise, outPromise]);
37
+
38
+ if (verbose) {
39
+ if (!status) {
40
+ console.log(`Command: ${commandLine} exited with status ${status}`);
41
+ } else {
42
+ console.error(`Command: ${commandLine} exited with status ${status}`);
43
+ }
44
+ }
45
+
46
+ if (outStream instanceof BufferWritableStream) {
47
+ return outStream.getText();
48
+ } else {
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+
54
+ export function executePipe(commands: Command[], finalOutput: Writable | undefined, verbose: boolean = false) {
55
+ return new Promise((resolve, reject) => {
56
+ let input: Stream | undefined;
57
+ let child: ChildProcess | undefined;
58
+ for (const cmd of commands) {
59
+ verbose && console.log(`Running: ${cmd.name} ${cmd.args?.join(' ')}`);
60
+ child = spawn(cmd.name, cmd.args, {
61
+ // in, out, err
62
+ stdio: ['pipe', 'pipe', 'inherit'],
63
+ });
64
+ if (input) {
65
+ input.pipe(child.stdin!);
66
+ }
67
+ input = child.stdout!;
68
+ child.on("error", (err: Error) => {
69
+ console.error(`Failed to run ${cmd.name}`, err);
70
+ reject(err);
71
+ })
72
+ }
73
+ if (child) {
74
+ child.on("exit", (code: number | null, signal: string | null) => {
75
+ resolve(code !== null ? code : signal);
76
+ });
77
+ finalOutput && child.stdout?.pipe(finalOutput);
78
+ } else {
79
+ reject(new Error("no child spawned"));
80
+ }
81
+ });
82
+
83
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { Builder, buildMemoryPack } from "./Builder.js"
2
+ export type { BuildOptions, Commands } from "./Builder.js"
3
+ export type { CopyOptions } from "./commands/copy.js"
4
+ export type { ExecOptions } from "./commands/exec.js"
5
+ export { ContentObject, DocxObject, JsonObject, MediaObject, PdfObject } from "./ContentObject.js"
6
+ export type { MediaOptions } from "./ContentObject.js"
7
+ export { AbstractContentSource, BufferSource, FileSource, TextSource } from "./ContentSource.js"
8
+ export type { ContentSource, SourceSpec } from "./ContentSource.js"
9
+ export { MemoryEntry, TarMemoryPack, loadMemoryPack } from "./MemoryPack.js"
10
+ export type { MemoryPack, ProjectionProperties } from "./MemoryPack.js"
11
+ export { MemoryPackBuilder } from "./MemoryPackBuilder.js"
12
+ export type { FromOptions } from "./MemoryPackBuilder.js"
13
+ export { TarBuilder, TarIndex, loadTarIndex } from './utils/tar.js'
14
+ export type { TarEntry, TarEntryIndex } from './utils/tar.js'
@@ -0,0 +1,32 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { splitCommandLine, splitPipeCommands } from "./cmdline";
3
+
4
+ describe("command line parser", () => {
5
+
6
+ test("split command line", () => {
7
+ const args = splitCommandLine("cmd -m \"hello 'world'\" 'some \"file\"' 'nested \"dquote\"' \"nested \'squote\'\"");
8
+ expect(args).toEqual(["cmd", "-m", "hello 'world'", "some \"file\"", 'nested "dquote"', "nested 'squote'"]);
9
+
10
+ })
11
+
12
+ test("split commands pipeline", () => {
13
+ const pipe = splitPipeCommands("cat \"the file.txt\" | grep \"the\" | wc -c");
14
+ expect(pipe.out).toBeUndefined();
15
+ expect(pipe.commands).toEqual([
16
+ { name: "cat", args: ["the file.txt"] },
17
+ { name: "grep", args: ["the"] },
18
+ { name: "wc", args: ["-c"] }
19
+ ]);
20
+ })
21
+
22
+ test("split commands pipeline and redirect output", () => {
23
+ const pipe = splitPipeCommands("cat \"the file.txt\" | grep \"the\" | wc -c > out.txt");
24
+ expect(pipe.out).toBe("out.txt");
25
+ expect(pipe.commands).toEqual([
26
+ { name: "cat", args: ["the file.txt"] },
27
+ { name: "grep", args: ["the"] },
28
+ { name: "wc", args: ["-c"] }
29
+ ]);
30
+ })
31
+
32
+ });
@@ -0,0 +1,92 @@
1
+
2
+ function readWord(text: string, index: number): [string, number] {
3
+ let i = index, len = text.length;
4
+ while (i < len && (text[i] !== ' ' || text[i] !== '\t')) {
5
+ const c = text[i];
6
+ if (c === ' ' || c === '\t') {
7
+ return [text.substring(index, i), i];
8
+ }
9
+ i++;
10
+ }
11
+ return [text.substring(index), len];
12
+ }
13
+
14
+ function readQuotedArg(text: string, index: number, quote: string): [string, number] {
15
+ let i = index, len = text.length;
16
+ while (i < len) {
17
+ const c = text[i];
18
+ if (c === '\\') {
19
+ i += 2;
20
+ continue;
21
+ } else if (c === quote) {
22
+ return [text.substring(index, i), i + 1];
23
+ } else {
24
+ i++;
25
+ }
26
+ }
27
+ return [text.substring(index), len];
28
+ }
29
+
30
+ export function splitCommandLine(text: string) {
31
+ const args = [];
32
+ let i = 0, len = text.length;
33
+ while (i < len) {
34
+ let c = text[i];
35
+ if (c === ' ' || c === '\t') {
36
+ i++; // skip whitespace
37
+ } else if (c === '"' || c === "'") {
38
+ const [word, offset] = readQuotedArg(text, i + 1, c);
39
+ i = offset;
40
+ args.push(word);
41
+ } else {
42
+ const [word, offset] = readWord(text, i);
43
+ i = offset;
44
+ args.push(word);
45
+ }
46
+ }
47
+ return args;
48
+ }
49
+
50
+ export interface Command {
51
+ name: string;
52
+ args: string[];
53
+ }
54
+ export interface CommandPipe {
55
+ commands: Command[];
56
+ out?: string | undefined;
57
+ }
58
+ export function splitPipeCommands(text: string): CommandPipe {
59
+ const tokens = splitCommandLine(text);
60
+ const commands: Command[] = [];
61
+ let args: string[] = [];
62
+ for (const token of tokens) {
63
+ if (token === "|") {
64
+ if (args.length < 1) {
65
+ throw new Error("Invalid pipe character. Expecting a command first.");
66
+ }
67
+ const name = args.shift()!;
68
+ commands.push({
69
+ name,
70
+ args
71
+ });
72
+ args = [];
73
+ } else {
74
+ args.push(token);
75
+ }
76
+ }
77
+ let out: string | undefined;
78
+ if (args.length > 0) {
79
+ const name = args.shift()!;
80
+ if (args.length > 1 && args[args.length - 2] === ">") {
81
+ out = args.pop();
82
+ args.pop(); // remove the ">"
83
+ }
84
+ commands.push({
85
+ name,
86
+ args
87
+ });
88
+ }
89
+ return {
90
+ commands, out
91
+ };
92
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createPathRewrite } from "./rewrite";
3
+
4
+ describe("Path rewrite", () => {
5
+ test("constant path", () => {
6
+ let fn = createPathRewrite("/root!some/path")
7
+ let r = fn("/root/the/path", 0)
8
+ expect(r).toBe("some/path")
9
+
10
+ fn = createPathRewrite("some/path")
11
+ r = fn("/root/the/path", 0)
12
+ expect(r).toBe("some/path")
13
+ })
14
+
15
+ test("wildcard path", () => {
16
+ let fn = createPathRewrite("/root!*")
17
+ let r = fn("/root/the/path.ext", 0)
18
+ expect(r).toBe("the/path.ext")
19
+
20
+ fn = createPathRewrite("*")
21
+ r = fn("/root/the/path.ext", 0)
22
+ expect(r).toBe("path.ext")
23
+ })
24
+
25
+ test("expression path", () => {
26
+ let fn = createPathRewrite("/root!images/%d/file-%n-%i.%e")
27
+ let r = fn("/root/the/path.ext", 0)
28
+ expect(r).toBe("images/the/file-path-0.ext")
29
+
30
+ fn = createPathRewrite("images/%d/file-%n-%i.%e")
31
+ r = fn("/root/the/path.ext", 0)
32
+ expect(r).toBe("images/file-path-0.ext")
33
+ })
34
+
35
+ test("expression path using %f", () => {
36
+ let fn = createPathRewrite("/root!images/%d/file-%i-%f")
37
+ let r = fn("/root/the/path.ext", 0)
38
+ expect(r).toBe("images/the/file-0-path.ext")
39
+
40
+ fn = createPathRewrite("images/%d/file-%i-%f")
41
+ r = fn("/root/the/path.ext", 0)
42
+ expect(r).toBe("images/file-0-path.ext")
43
+ })
44
+
45
+ test("expression using widlcard with prefix", () => {
46
+ let fn = createPathRewrite("/root!images/*")
47
+ let r = fn("/root/the/path.ext", 0)
48
+ expect(r).toBe("images/the/path.ext")
49
+
50
+ fn = createPathRewrite("images/*")
51
+ r = fn("/root/the/path.ext", 0)
52
+ expect(r).toBe("images/path.ext")
53
+ })
54
+
55
+ test("expression using %p", () => {
56
+ let fn = createPathRewrite("/root!images/%p")
57
+ let r = fn("/root/the/path.ext", 0)
58
+ expect(r).toBe("images/the_path.ext")
59
+
60
+ fn = createPathRewrite("images/%p")
61
+ r = fn("/root/the/path.ext", 0)
62
+ expect(r).toBe("images/path.ext")
63
+ })
64
+
65
+ })
@@ -0,0 +1,167 @@
1
+ import { basename, dirname, extname, join } from "path";
2
+
3
+ /**
4
+ * The path argumentmay is the empty string when mapping streams or buffers not related to a file system file.
5
+ */
6
+ export type PathMapperFn = ((path: string, index: number) => string);
7
+
8
+ export function createPathRewrite(path: string): PathMapperFn {
9
+ let truncPath: (path: string) => string;
10
+ let basePath: string = '';
11
+ let index = path.indexOf('!');
12
+ if (index > -1) {
13
+ basePath = path.substring(0, index);
14
+ if (!basePath.endsWith('/')) {
15
+ basePath += '/';
16
+ }
17
+ truncPath = (path: string) => {
18
+ return path.substring(basePath.length);
19
+ }
20
+ path = path.substring(index + 1);
21
+ } else {
22
+ truncPath = (path: string) => {
23
+ return basename(path);
24
+ }
25
+ }
26
+ if (path === '*') {
27
+ // preserve path
28
+ return truncPath;
29
+ } else if (path.endsWith("/*")) {
30
+ const prefix = path.slice(0, -2);
31
+ return (path: string) => {
32
+ path = truncPath(path);
33
+ return join(prefix, path);
34
+ }
35
+ } else {
36
+ // use path builder
37
+ return buildPathRewrite(path, truncPath);
38
+ }
39
+ }
40
+
41
+ const RX_PARTS = /(%d\/)|(\.?%e)|(%[fnip])/g;
42
+ function buildPathRewrite(path: string, truncPath: (path: string) => string): PathMapperFn {
43
+ let parts: ((path: Path, index: number) => string)[] = [];
44
+ let m: RegExpExecArray | null;
45
+ let lastIndex = 0;
46
+ while (m = RX_PARTS.exec(path)) {
47
+ if (m.index > lastIndex) {
48
+ const literal = path.substring(lastIndex, m.index);
49
+ parts.push(() => literal);
50
+ }
51
+ if (m[1]) { // %d/
52
+ parts.push((path: Path) => path.dirname ? path.dirname + '/' : '');
53
+ } else if (m[2]) { // .?%e
54
+ if (m[2][0] === '.') {
55
+ parts.push((path: Path) => path.extname || '');
56
+ } else {
57
+ parts.push((path: Path) => path.extname ? path.extname.slice(1) : ''); // extension without dot
58
+ }
59
+ } else if (m[3]) {
60
+ switch (m[3]) {
61
+ case '%f':
62
+ parts.push((path: Path) => path.name);
63
+ break;
64
+ case '%n':
65
+ parts.push((path: Path) => path.basename);
66
+ break;
67
+ case '%p': // stringify the path by replacing / with _
68
+ parts.push((path: Path) => {
69
+ let p = path.value;
70
+ if (p.startsWith('/')) {
71
+ p = p.substring(1);
72
+ }
73
+ return p.replaceAll('/', '_');
74
+ });
75
+ break;
76
+ case '%i': // index
77
+ parts.push((_path: Path, index: number) => String(index));
78
+ break;
79
+ default: throw new Error(`Bug: should never happen`);
80
+ }
81
+ }
82
+ lastIndex = m.index + m[0].length;
83
+ }
84
+ if (!parts.length) {
85
+ return () => path;
86
+ } else {
87
+ if (lastIndex < path.length) {
88
+ const literal = path.substring(lastIndex);
89
+ parts.push(() => literal);
90
+ }
91
+ return (path: string, index: number) => {
92
+ const pathObj = new Path(truncPath(path));
93
+ const out = [];
94
+ for (const part of parts) {
95
+ out.push(part(pathObj, index));
96
+ }
97
+ return out.join('');
98
+ }
99
+ }
100
+ }
101
+
102
+
103
+ export class Path {
104
+ _name?: string;
105
+ _extname?: string;
106
+ _dirname?: string;
107
+ _basename?: string;
108
+
109
+ /**
110
+ * The complete path value
111
+ */
112
+ value: string
113
+
114
+ /**
115
+ * The file name (the last portion of the path). Includes the extension if present.
116
+ */
117
+ get name(): string {
118
+ if (!this._name) {
119
+ this._name = basename(this.value);
120
+ }
121
+ return this._name;
122
+ }
123
+
124
+ /**
125
+ * The extension of the file including the leading '.'.
126
+ * An empty string if the file has no extension.
127
+ */
128
+ get extname(): string {
129
+ if (!this._extname) {
130
+ this._extname = extname(this.value);
131
+ }
132
+ return this._extname;
133
+ }
134
+ /**
135
+ * The directory portion of the path. Doesn'r include the trailing slash.
136
+ * If no directory is present, returns an empty string.
137
+ */
138
+ get dirname(): string {
139
+ if (!this._dirname) {
140
+ this._dirname = dirname(this.value);
141
+ if (this._dirname === '.') {
142
+ this._dirname = '';
143
+ }
144
+ }
145
+ return this._dirname;
146
+ }
147
+ /**
148
+ * The path without the extension
149
+ */
150
+ get basename(): string {
151
+ if (!this._basename) {
152
+ this._basename = this.extname ? this.name.slice(0, -this.extname.length) : this.name;
153
+ }
154
+ return this._basename;
155
+ }
156
+
157
+ constructor(value: string) {
158
+ this.value = value;
159
+ }
160
+
161
+ /**
162
+ * Return the complete path value (same as `value`)
163
+ */
164
+ toString(): string {
165
+ return this.value;
166
+ }
167
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { BufferWritableStream } from "./stream";
3
+
4
+ describe("BufferWritableStream", () => {
5
+ test("write buffer", () => {
6
+ const stream = new BufferWritableStream();
7
+ stream.write(Buffer.from("hello"));
8
+ stream.write(Buffer.from(" "));
9
+ stream.write(Buffer.from("world"));
10
+ stream.end();
11
+ expect(stream.getText()).toBe("hello world");
12
+ })
13
+ })
@@ -0,0 +1,27 @@
1
+ import { Writable } from "stream";
2
+
3
+ export class BufferWritableStream extends Writable {
4
+ chunks: Buffer[] = []
5
+ buffer: Buffer | undefined;
6
+
7
+ // _write method is required to handle the incoming data
8
+ _write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void) {
9
+ this.chunks.push(chunk); // Collect the chunk into the array
10
+ callback(); // Indicate that the write is complete
11
+ }
12
+
13
+ // Optional _final method is called when the stream is ending
14
+ _final(callback: (error?: Error | null) => void) {
15
+ this.buffer = Buffer.concat(this.chunks); // Concatenate the collected chunks into a buffer
16
+ callback(); // Indicate the stream is finished
17
+ }
18
+
19
+ // Method to get the final buffer when the stream is closed
20
+ getBuffer() {
21
+ return this.buffer;
22
+ }
23
+
24
+ getText(encoding: BufferEncoding = "utf-8") {
25
+ return this.buffer?.toString(encoding);
26
+ }
27
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
+ import { loadTarIndex, TarBuilder, TarIndex } from "./tar";
3
+ import { readFileSync, stat, statSync, unlinkSync } from "fs";
4
+
5
+ const tarFile = `test-${Date.now()}.tar`;
6
+ afterAll(() => {
7
+ unlinkSync(tarFile);
8
+ });
9
+
10
+ describe("Indexed tar format", () => {
11
+ const builder = new TarBuilder(tarFile);
12
+ test("build tar", async () => {
13
+ builder.add("file1.txt", Buffer.from("hello world!"));
14
+ builder.add("file2.txt", Buffer.from("bonjour monde!"));
15
+ builder.add("app/package.json", readFileSync("./package.json"));
16
+ await builder.build();
17
+ const stats = statSync(tarFile);
18
+ expect(stats.isFile()).toBeTruthy();
19
+ });
20
+
21
+ test("read tar", async () => {
22
+ const index = await loadTarIndex(tarFile) as TarIndex;
23
+ expect(index).toBeDefined();
24
+ expect(Object.keys(index.entries).length).toBe(3);
25
+ const file1 = index.get('file1.txt');
26
+ const file2 = index.get('file2.txt');
27
+ const file3 = index.get('app/package.json');
28
+ expect(file1).toBeDefined();
29
+ expect(file2).toBeDefined();
30
+ expect(file3).toBeDefined();
31
+ expect(file1!.size).toBe(12);
32
+ expect(file2!.size).toBe(14);
33
+ expect(file3!.size).toBeGreaterThan(0);
34
+
35
+ const content1 = await index.getContent('file1.txt');
36
+ const content2 = await index.getContent('file2.txt');
37
+ const content3 = await index.getContent('app/package.json');
38
+
39
+ expect(content1!.toString()).toBe("hello world!");
40
+ expect(content2!.toString()).toBe("bonjour monde!");
41
+ const pkg = JSON.parse(content3!.toString());
42
+ expect(pkg).toBeDefined();
43
+ expect(pkg).toHaveProperty("name");
44
+ expect(pkg).toHaveProperty("version");
45
+ expect(pkg).toHaveProperty("dependencies");
46
+ });
47
+
48
+ });