@vertesia/memory 0.24.0-dev.202601221707
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/LICENSE +13 -0
- package/README.md +473 -0
- package/lib/cjs/Builder.js +186 -0
- package/lib/cjs/Builder.js.map +1 -0
- package/lib/cjs/ContentObject.js +114 -0
- package/lib/cjs/ContentObject.js.map +1 -0
- package/lib/cjs/ContentSource.js +82 -0
- package/lib/cjs/ContentSource.js.map +1 -0
- package/lib/cjs/MemoryPack.js +228 -0
- package/lib/cjs/MemoryPack.js.map +1 -0
- package/lib/cjs/MemoryPackBuilder.js +47 -0
- package/lib/cjs/MemoryPackBuilder.js.map +1 -0
- package/lib/cjs/commands/copy.js +53 -0
- package/lib/cjs/commands/copy.js.map +1 -0
- package/lib/cjs/commands/exec.js +82 -0
- package/lib/cjs/commands/exec.js.map +1 -0
- package/lib/cjs/index.js +28 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/cjs/package.json +3 -0
- package/lib/cjs/utils/cmdline.js +90 -0
- package/lib/cjs/utils/cmdline.js.map +1 -0
- package/lib/cjs/utils/rewrite.js +166 -0
- package/lib/cjs/utils/rewrite.js.map +1 -0
- package/lib/cjs/utils/stream.js +27 -0
- package/lib/cjs/utils/stream.js.map +1 -0
- package/lib/cjs/utils/tar.js +185 -0
- package/lib/cjs/utils/tar.js.map +1 -0
- package/lib/esm/Builder.js +178 -0
- package/lib/esm/Builder.js.map +1 -0
- package/lib/esm/ContentObject.js +103 -0
- package/lib/esm/ContentObject.js.map +1 -0
- package/lib/esm/ContentSource.js +75 -0
- package/lib/esm/ContentSource.js.map +1 -0
- package/lib/esm/MemoryPack.js +218 -0
- package/lib/esm/MemoryPack.js.map +1 -0
- package/lib/esm/MemoryPackBuilder.js +43 -0
- package/lib/esm/MemoryPackBuilder.js.map +1 -0
- package/lib/esm/commands/copy.js +50 -0
- package/lib/esm/commands/copy.js.map +1 -0
- package/lib/esm/commands/exec.js +75 -0
- package/lib/esm/commands/exec.js.map +1 -0
- package/lib/esm/index.js +7 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/esm/utils/cmdline.js +86 -0
- package/lib/esm/utils/cmdline.js.map +1 -0
- package/lib/esm/utils/rewrite.js +161 -0
- package/lib/esm/utils/rewrite.js.map +1 -0
- package/lib/esm/utils/stream.js +23 -0
- package/lib/esm/utils/stream.js.map +1 -0
- package/lib/esm/utils/tar.js +175 -0
- package/lib/esm/utils/tar.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/Builder.d.ts +72 -0
- package/lib/types/Builder.d.ts.map +1 -0
- package/lib/types/ContentObject.d.ts +43 -0
- package/lib/types/ContentObject.d.ts.map +1 -0
- package/lib/types/ContentSource.d.ts +32 -0
- package/lib/types/ContentSource.d.ts.map +1 -0
- package/lib/types/MemoryPack.d.ts +46 -0
- package/lib/types/MemoryPack.d.ts.map +1 -0
- package/lib/types/MemoryPackBuilder.d.ts +18 -0
- package/lib/types/MemoryPackBuilder.d.ts.map +1 -0
- package/lib/types/commands/copy.d.ts +8 -0
- package/lib/types/commands/copy.d.ts.map +1 -0
- package/lib/types/commands/exec.d.ts +7 -0
- package/lib/types/commands/exec.d.ts.map +1 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/utils/cmdline.d.ts +10 -0
- package/lib/types/utils/cmdline.d.ts.map +1 -0
- package/lib/types/utils/rewrite.d.ts +38 -0
- package/lib/types/utils/rewrite.d.ts.map +1 -0
- package/lib/types/utils/stream.d.ts +9 -0
- package/lib/types/utils/stream.d.ts.map +1 -0
- package/lib/types/utils/tar.d.ts +40 -0
- package/lib/types/utils/tar.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/Builder.ts +239 -0
- package/src/ContentObject.ts +114 -0
- package/src/ContentSource.ts +88 -0
- package/src/MemoryPack.ts +233 -0
- package/src/MemoryPackBuilder.ts +55 -0
- package/src/builder.test.ts +214 -0
- package/src/commands/copy.ts +53 -0
- package/src/commands/exec.test.ts +22 -0
- package/src/commands/exec.ts +83 -0
- package/src/index.ts +14 -0
- package/src/utils/cmdline.test.ts +32 -0
- package/src/utils/cmdline.ts +92 -0
- package/src/utils/rewrite.test.ts +65 -0
- package/src/utils/rewrite.ts +167 -0
- package/src/utils/stream.test.ts +13 -0
- package/src/utils/stream.ts +27 -0
- package/src/utils/tar.test.ts +48 -0
- package/src/utils/tar.ts +203 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Builder } from "./Builder.js";
|
|
2
|
+
import { ContentSource } from "./ContentSource.js";
|
|
3
|
+
import { MEMORY_METADATA_ENTRY, MemoryPack, ProjectionProperties } from "./MemoryPack.js";
|
|
4
|
+
import { normalizePath, TarBuilder } from "./utils/tar.js";
|
|
5
|
+
|
|
6
|
+
export interface FromOptions {
|
|
7
|
+
files?: string[];
|
|
8
|
+
projection?: ProjectionProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class MemoryPackBuilder {
|
|
12
|
+
baseMetadata: Record<string, any> = {};
|
|
13
|
+
entries: { [path: string]: ContentSource } = {};
|
|
14
|
+
|
|
15
|
+
constructor(public builder: Builder) {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async load(memory: MemoryPack, options: FromOptions = {}) {
|
|
19
|
+
const files = options.files || [];
|
|
20
|
+
// do not fetch the context entry an the metadata.json file
|
|
21
|
+
files.push(`!${MEMORY_METADATA_ENTRY}`);
|
|
22
|
+
const entries = memory.getEntries(files);
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
this.add(entry.name, entry);
|
|
25
|
+
}
|
|
26
|
+
this.baseMetadata = await memory.getMetadata(options.projection);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
add(path: string, content: ContentSource) {
|
|
30
|
+
path = normalizePath(path);
|
|
31
|
+
this.entries[path] = content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async build(file: string, metadata: object) {
|
|
35
|
+
metadata = { ...this.baseMetadata, ...metadata };
|
|
36
|
+
if (!file.endsWith('.tar')) {
|
|
37
|
+
file += '.tar';
|
|
38
|
+
}
|
|
39
|
+
if (this.builder.options.gzip) {
|
|
40
|
+
file += ".gz";
|
|
41
|
+
}
|
|
42
|
+
const tar = new TarBuilder(file);
|
|
43
|
+
const keys = Object.keys(this.entries).sort();
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const source = this.entries[key];
|
|
46
|
+
tar.add(key, await source.getContent());
|
|
47
|
+
}
|
|
48
|
+
tar.add(MEMORY_METADATA_ENTRY, Buffer.from(
|
|
49
|
+
JSON.stringify(metadata, undefined, this.builder.options.indent || undefined),
|
|
50
|
+
"utf-8")
|
|
51
|
+
);
|
|
52
|
+
await tar.build();
|
|
53
|
+
return file;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, statSync } from "fs";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
3
|
+
import { Builder } from "./Builder";
|
|
4
|
+
import { loadTarIndex } from "./utils/tar";
|
|
5
|
+
import { loadMemoryPack, MEMORY_METADATA_ENTRY } from "./MemoryPack";
|
|
6
|
+
|
|
7
|
+
const memoryBaseFile = 'test-base-memory.tar';
|
|
8
|
+
const memoryFile = 'test-memory.tar';
|
|
9
|
+
const tmpdir = mkdtempSync("composable-memory-pack-test-");
|
|
10
|
+
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
rmSync(memoryBaseFile);
|
|
13
|
+
rmSync(memoryFile);
|
|
14
|
+
rmSync(tmpdir, { recursive: true });
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe("Builder", () => {
|
|
18
|
+
test("create base memory pack", async () => {
|
|
19
|
+
const builder = new Builder({
|
|
20
|
+
out: memoryBaseFile,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await builder.exec(`printf 'file1 from base memory' > ${tmpdir}/file1.txt`);
|
|
24
|
+
const file2 = await builder.exec('printf "file2 from base memory"', { quiet: true });
|
|
25
|
+
builder.copy(`${tmpdir}/file1.txt`, "file1.txt");
|
|
26
|
+
builder.copyText(file2 || '', "file2.txt");
|
|
27
|
+
builder.copyText("file3 from base memory", "file3.txt");
|
|
28
|
+
|
|
29
|
+
await builder.build({
|
|
30
|
+
baseProp1: "baseProp1",
|
|
31
|
+
baseProp2: "baseProp2",
|
|
32
|
+
baseProp3: "baseProp3",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const stats = statSync(memoryBaseFile);
|
|
36
|
+
expect(stats.isFile()).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("create memory pack override", async () => {
|
|
40
|
+
const builder = new Builder({
|
|
41
|
+
out: memoryFile,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await builder.from(memoryBaseFile, {
|
|
45
|
+
files: ["!file1.txt"], // remove file1
|
|
46
|
+
projection: {
|
|
47
|
+
baseProp1: false, // remove baseProp1
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
// override file2
|
|
51
|
+
builder.copyText(`file2 from new memory`, "file2.txt");
|
|
52
|
+
builder.copyText(`file4 from new memory`, "file4.txt");
|
|
53
|
+
|
|
54
|
+
await builder.build({
|
|
55
|
+
baseProp2: "baseProp2", // override baseProp2
|
|
56
|
+
prop4: "prop4",
|
|
57
|
+
parent: { child: 123 }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const stats = statSync(memoryFile);
|
|
61
|
+
expect(stats.isFile()).toBeTruthy();
|
|
62
|
+
|
|
63
|
+
// check the content of the tar
|
|
64
|
+
|
|
65
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
66
|
+
const entries = memory.getEntries();
|
|
67
|
+
expect(entries.length).toBe(4);
|
|
68
|
+
expect(entries.map(e => e.name).sort()).toStrictEqual(["file2.txt", "file3.txt", "file4.txt", MEMORY_METADATA_ENTRY].sort());
|
|
69
|
+
|
|
70
|
+
const entry1 = memory.getEntry("file1.txt");
|
|
71
|
+
expect(entry1).toBeNull();
|
|
72
|
+
const entry2 = memory.getEntry("file2.txt");
|
|
73
|
+
expect(entry2).not.toBeNull();
|
|
74
|
+
const entry3 = memory.getEntry("file3.txt");
|
|
75
|
+
expect(entry3).not.toBeNull();
|
|
76
|
+
const entry4 = memory.getEntry("file4.txt");
|
|
77
|
+
expect(entry4).not.toBeNull();
|
|
78
|
+
|
|
79
|
+
const content2 = (await entry2?.getContent())?.toString("utf-8");
|
|
80
|
+
const content3 = (await entry3?.getContent())?.toString("utf-8");
|
|
81
|
+
const content4 = (await entry4?.getContent())?.toString("utf-8");
|
|
82
|
+
|
|
83
|
+
expect(content2).toBe("file2 from new memory");
|
|
84
|
+
expect(content3).toBe("file3 from base memory");
|
|
85
|
+
expect(content4).toBe("file4 from new memory");
|
|
86
|
+
|
|
87
|
+
const context = await memory.getMetadata();
|
|
88
|
+
expect(context).toStrictEqual({
|
|
89
|
+
baseProp2: "baseProp2",
|
|
90
|
+
baseProp3: "baseProp3",
|
|
91
|
+
prop4: "prop4",
|
|
92
|
+
parent: { child: 123 }
|
|
93
|
+
});
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("MemoryPack.exportObject", async () => {
|
|
97
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
98
|
+
let obj = await memory.exportObject({
|
|
99
|
+
"@": "@",
|
|
100
|
+
"file2": "@content:file2.txt",
|
|
101
|
+
});
|
|
102
|
+
expect(obj).toStrictEqual({
|
|
103
|
+
"file2": "file2 from new memory",
|
|
104
|
+
baseProp2: "baseProp2",
|
|
105
|
+
baseProp3: "baseProp3",
|
|
106
|
+
prop4: "prop4",
|
|
107
|
+
parent: { child: 123 }
|
|
108
|
+
})
|
|
109
|
+
obj = await memory.exportObject({
|
|
110
|
+
"prop4": "@prop4",
|
|
111
|
+
"file2": "@content:file2.txt",
|
|
112
|
+
"manifest": "@",
|
|
113
|
+
});
|
|
114
|
+
expect(obj).toStrictEqual({
|
|
115
|
+
prop4: "prop4",
|
|
116
|
+
"file2": "file2 from new memory",
|
|
117
|
+
manifest: {
|
|
118
|
+
baseProp2: "baseProp2",
|
|
119
|
+
baseProp3: "baseProp3",
|
|
120
|
+
prop4: "prop4",
|
|
121
|
+
parent: { child: 123 }
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
obj = await memory.exportObject({
|
|
125
|
+
"content": "@content:*.txt",
|
|
126
|
+
});
|
|
127
|
+
expect(obj).toStrictEqual({
|
|
128
|
+
content: [
|
|
129
|
+
"file2 from new memory",
|
|
130
|
+
"file3 from base memory",
|
|
131
|
+
"file4 from new memory"
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
obj = await memory.exportObject({
|
|
136
|
+
"content": "@content:file2.txt,file3.txt",
|
|
137
|
+
});
|
|
138
|
+
expect(obj).toStrictEqual({
|
|
139
|
+
content: [
|
|
140
|
+
"file2 from new memory",
|
|
141
|
+
"file3 from base memory",
|
|
142
|
+
]
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
obj = await memory.exportObject({
|
|
146
|
+
"content": "@content:file2.txt,*4.txt",
|
|
147
|
+
});
|
|
148
|
+
expect(obj).toStrictEqual({
|
|
149
|
+
content: [
|
|
150
|
+
"file2 from new memory",
|
|
151
|
+
"file4 from new memory"
|
|
152
|
+
]
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
obj = await memory.exportObject({
|
|
156
|
+
"files": "@file:*.txt",
|
|
157
|
+
});
|
|
158
|
+
expect(obj).toStrictEqual({
|
|
159
|
+
files: [
|
|
160
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
161
|
+
{ name: "file3.txt", content: "file3 from base memory" },
|
|
162
|
+
{ name: "file4.txt", content: "file4 from new memory" }
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
obj = await memory.exportObject({
|
|
167
|
+
"files": "@file:file2.txt,file3.txt",
|
|
168
|
+
});
|
|
169
|
+
expect(obj).toStrictEqual({
|
|
170
|
+
files: [
|
|
171
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
172
|
+
{ name: "file3.txt", content: "file3 from base memory" },
|
|
173
|
+
]
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
obj = await memory.exportObject({
|
|
177
|
+
"files": "@file:*2.txt,*4.txt",
|
|
178
|
+
});
|
|
179
|
+
expect(obj).toStrictEqual({
|
|
180
|
+
files: [
|
|
181
|
+
{ name: "file2.txt", content: "file2 from new memory" },
|
|
182
|
+
{ name: "file4.txt", content: "file4 from new memory" },
|
|
183
|
+
]
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("MemoryPack.exportObject with nested props", async () => {
|
|
189
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
190
|
+
let obj = await memory.exportObject({
|
|
191
|
+
"child": "@parent.child",
|
|
192
|
+
});
|
|
193
|
+
expect(obj).toStrictEqual({
|
|
194
|
+
child: 123
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("MemoryPack.exportObject with runtime values", async () => {
|
|
199
|
+
const memory = await loadMemoryPack(memoryFile);
|
|
200
|
+
let obj = await memory.exportObject({
|
|
201
|
+
"father": "@parent",
|
|
202
|
+
"instruction": "Use this runtime instruction",
|
|
203
|
+
someNullValue: null,
|
|
204
|
+
someObject: { msg: "Hello" }
|
|
205
|
+
});
|
|
206
|
+
expect(obj).toStrictEqual({
|
|
207
|
+
father: { child: 123 },
|
|
208
|
+
"instruction": "Use this runtime instruction",
|
|
209
|
+
someNullValue: null,
|
|
210
|
+
someObject: { msg: "Hello" }
|
|
211
|
+
})
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
});
|
|
@@ -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 specified");
|
|
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 wildcard 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
|
+
})
|