-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpipeline.ts
More file actions
136 lines (110 loc) · 3.48 KB
/
pipeline.ts
File metadata and controls
136 lines (110 loc) · 3.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { FileTree } from "./filetree.js"
class PipelineFile {
path: string
content: Buffer
constructor(path: string, content: string | Buffer) {
this.path = path
this.content = typeof content === 'string' ? Buffer.from(content) : content
}
#text?: string | undefined
get text() { return this.#text ??= this.content.toString() }
set text(s) { this.#text = s }
textOrContent() { return this.#text ?? this.content }
copy(path = this.path) {
const copy = new PipelineFile(path, this.content)
copy.#text = this.#text
return copy
}
}
type Filter = { regex: RegExp, negate: boolean }
/**
* The Pipeline API is convenience class for
* arbitrarily manipulating an array of files,
* usually to produce a front-end file tree,
* but isn't specific to that purpose.
*
* You don't need a pipeline object, you can just
* manually * transform a `FileTree.files` into a map
* compatible with `DevServer` and `generateFiles`.
*
* But that gets very inconvenient very quickly:
*
* * Every `file.content` is always a Buffer, but sometimes
* we want to lazily transform it to a string and use that
*
* * Filtering files is almost always path-based,
* and would be much more convenient with regexes
*
* * Adding and removing individual files or arrays of files
* is technically possible but *very* repetitious
*
* So the `Pipeline` class was created as a convenience.
*/
export class Pipeline {
static from(files: FileTree['files']) {
const initial = [...files.values().map(f => new PipelineFile(f.path, f.content))]
const pl = new Pipeline()
pl.#real = initial
return pl
}
#real: PipelineFile[] = []
#filters: Filter[] = []
private static create(files: PipelineFile[], filters: Filter[]) {
const pl = new Pipeline()
pl.#real = files
pl.#filters = filters
return pl
}
all() {
return this.#real.filter(file => this.#matches(file))
}
copy() {
return Pipeline.create(this.#real.map(f => f.copy()), this.#filters)
}
#matches(file: PipelineFile): boolean {
return this.#filters.every(f => f.regex.test(file.path) === !f.negate)
}
add(path: string, content: string | Buffer | PipelineFile) {
this.#real.push(content instanceof PipelineFile
? content.copy(path)
: new PipelineFile(path, content))
}
graft(prefix: string, files: Pipeline | FileTree) {
if (files instanceof FileTree) files = Pipeline.from(files.files)
files.do(f => this.add(prefix + f.path, f))
}
del(path: string) {
const idx = this.#real.findIndex(f => f.path === path)
if (idx !== -1) this.#real.splice(idx, 1)
}
with(regex: RegExp | string) {
return Pipeline.create(this.#real, [...this.#filters, { regex: ensureRegex(regex), negate: false }])
}
without(regex: RegExp | string) {
return Pipeline.create(this.#real, [...this.#filters, { regex: ensureRegex(regex), negate: true }])
}
remove() {
let i = this.#real.length
while (i--) {
const file = this.#real[i]!
if (this.#matches(file)) {
this.#real.splice(i, 1)
}
}
}
async doAsync(fn: (file: PipelineFile) => void | Promise<void>) {
await Promise.all(this.all().map(fn))
}
do(fn: (file: PipelineFile) => void) {
this.all().map(fn)
}
paths() {
return this.all().map(f => f.path)
}
results() {
return new Map(this.all().map(f => [f.path, { content: f.textOrContent() }]))
}
}
function ensureRegex(s: RegExp | string) {
return typeof s === 'string' ? new RegExp(s) : s
}