@eighty4/dank 0.0.5-1 → 0.0.5-3
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/lib/build.ts +1 -2
- package/lib/build_tag.ts +119 -15
- package/lib/config.ts +28 -0
- package/lib/dank.ts +10 -2
- package/lib/dirs.ts +8 -9
- package/lib/http.ts +9 -10
- package/lib/serve.ts +65 -21
- package/lib/services.ts +242 -204
- package/lib/watch.ts +38 -7
- package/lib_js/build.js +1 -2
- package/lib_js/build_tag.js +74 -14
- package/lib_js/config.js +20 -0
- package/lib_js/dirs.js +6 -6
- package/lib_js/http.js +8 -9
- package/lib_js/serve.js +45 -14
- package/lib_js/services.js +158 -169
- package/lib_js/watch.js +14 -5
- package/lib_types/dank.d.ts +5 -0
- package/package.json +1 -1
package/lib/services.ts
CHANGED
|
@@ -4,128 +4,225 @@ import {
|
|
|
4
4
|
execSync,
|
|
5
5
|
spawn,
|
|
6
6
|
} from 'node:child_process'
|
|
7
|
+
import EventEmitter from 'node:events'
|
|
7
8
|
import { basename, isAbsolute, resolve } from 'node:path'
|
|
8
9
|
import type { DevService, ResolvedDankConfig } from './config.ts'
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export class ManagedServiceLabel {
|
|
12
|
+
#command: string
|
|
13
|
+
#cwd: string
|
|
14
|
+
|
|
15
|
+
constructor(spec: DevService) {
|
|
16
|
+
this.#command = spec.command
|
|
17
|
+
this.#cwd = !spec.cwd
|
|
18
|
+
? './'
|
|
19
|
+
: spec.cwd.startsWith('/')
|
|
20
|
+
? `/.../${basename(spec.cwd)}`
|
|
21
|
+
: spec.cwd.startsWith('.')
|
|
22
|
+
? spec.cwd
|
|
23
|
+
: `./${spec.cwd}`
|
|
24
|
+
}
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
get command(): string {
|
|
27
|
+
return this.#command
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get cwd(): string {
|
|
31
|
+
return this.#cwd
|
|
32
|
+
}
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
export type HttpService = NonNullable<DevService['http']>
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
export type DevServiceEvents = {
|
|
38
|
+
error: [label: ManagedServiceLabel, cause: string]
|
|
39
|
+
exit: [label: ManagedServiceLabel, code: number | string]
|
|
40
|
+
launch: [label: ManagedServiceLabel]
|
|
41
|
+
stdout: [label: ManagedServiceLabel, output: Array<string>]
|
|
42
|
+
stderr: [label: ManagedServiceLabel, output: Array<string>]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class ManagedService extends EventEmitter<DevServiceEvents> {
|
|
46
|
+
#label: ManagedServiceLabel
|
|
47
|
+
#process: ChildProcess | null
|
|
48
|
+
#spec: DevService
|
|
49
|
+
// #status: ManagedServiceStatus = 'starting'
|
|
50
|
+
|
|
51
|
+
constructor(spec: DevService) {
|
|
52
|
+
super()
|
|
53
|
+
this.#label = new ManagedServiceLabel(spec)
|
|
54
|
+
this.#spec = spec
|
|
55
|
+
this.#process = this.#start()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get spec(): DevService {
|
|
59
|
+
return this.#spec
|
|
60
|
+
}
|
|
22
61
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
get httpSpec(): HttpService | undefined {
|
|
63
|
+
return this.#spec.http
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
matches(other: DevService): boolean {
|
|
67
|
+
return matchingConfig(this.#spec, other)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
kill() {
|
|
71
|
+
if (this.#process) killProcess(this.#process)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#start(): ChildProcess {
|
|
75
|
+
const { path, args } = parseCommand(this.#spec.command)
|
|
76
|
+
const env = this.#spec.env
|
|
77
|
+
? { ...process.env, ...this.#spec.env }
|
|
78
|
+
: undefined
|
|
79
|
+
const cwd =
|
|
80
|
+
!this.#spec.cwd || isAbsolute(this.#spec.cwd)
|
|
81
|
+
? this.#spec.cwd
|
|
82
|
+
: resolve(process.cwd(), this.#spec.cwd)
|
|
83
|
+
const spawned = spawnProcess(path, args, env, cwd)
|
|
84
|
+
this.emit('launch', this.#label)
|
|
85
|
+
spawned.stdout.on('data', chunk =>
|
|
86
|
+
this.emit('stdout', this.#label, parseChunk(chunk)),
|
|
87
|
+
)
|
|
88
|
+
spawned.stderr.on('data', chunk =>
|
|
89
|
+
this.emit('stderr', this.#label, parseChunk(chunk)),
|
|
90
|
+
)
|
|
91
|
+
spawned.on('error', e => {
|
|
92
|
+
if (e.name === 'AbortError') {
|
|
93
|
+
return
|
|
30
94
|
}
|
|
31
|
-
|
|
32
|
-
|
|
95
|
+
const cause =
|
|
96
|
+
'code' in e && e.code === 'ENOENT'
|
|
97
|
+
? 'program not found'
|
|
98
|
+
: e.message
|
|
99
|
+
this.emit('error', this.#label, cause)
|
|
100
|
+
})
|
|
101
|
+
spawned.on('exit', (code, signal) =>
|
|
102
|
+
this.emit('exit', this.#label, code || signal!),
|
|
103
|
+
)
|
|
104
|
+
return spawned
|
|
105
|
+
}
|
|
33
106
|
}
|
|
34
107
|
|
|
35
|
-
|
|
108
|
+
type SpawnProcess = (
|
|
109
|
+
program: string,
|
|
110
|
+
args: Array<string>,
|
|
111
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
112
|
+
cwd: string | undefined,
|
|
113
|
+
) => ChildProcessWithoutNullStreams
|
|
114
|
+
|
|
115
|
+
type KillProcess = (p: ChildProcess) => void
|
|
116
|
+
|
|
117
|
+
const killProcess: KillProcess =
|
|
118
|
+
process.platform === 'win32'
|
|
119
|
+
? p => execSync(`taskkill /pid ${p.pid} /T /F`)
|
|
120
|
+
: p => p.kill()
|
|
36
121
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
122
|
+
const spawnProcess: SpawnProcess =
|
|
123
|
+
process.platform === 'win32'
|
|
124
|
+
? (
|
|
125
|
+
path: string,
|
|
126
|
+
args: Array<string>,
|
|
127
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
128
|
+
cwd: string | undefined,
|
|
129
|
+
) =>
|
|
130
|
+
spawn('cmd', ['/c', path, ...args], {
|
|
131
|
+
cwd,
|
|
132
|
+
env,
|
|
133
|
+
detached: false,
|
|
134
|
+
shell: false,
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
})
|
|
137
|
+
: (
|
|
138
|
+
path: string,
|
|
139
|
+
args: Array<string>,
|
|
140
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
141
|
+
cwd: string | undefined,
|
|
142
|
+
) =>
|
|
143
|
+
spawn(path, args, {
|
|
144
|
+
cwd,
|
|
145
|
+
env,
|
|
146
|
+
detached: false,
|
|
147
|
+
shell: false,
|
|
148
|
+
})
|
|
42
149
|
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
150
|
+
export class DevServices extends EventEmitter<DevServiceEvents> {
|
|
151
|
+
#running: Array<ManagedService>
|
|
152
|
+
|
|
153
|
+
constructor(services: ResolvedDankConfig['services']) {
|
|
154
|
+
super()
|
|
155
|
+
this.#running = services ? this.#start(services) : []
|
|
156
|
+
if (process.platform === 'win32') {
|
|
157
|
+
process.once('SIGINT', () => process.exit())
|
|
51
158
|
}
|
|
159
|
+
process.once('exit', this.shutdown)
|
|
52
160
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return running.map(({ s }) => s.http).filter(http => !!http)
|
|
57
|
-
},
|
|
58
|
-
},
|
|
161
|
+
|
|
162
|
+
get httpServices(): Array<HttpService> {
|
|
163
|
+
return this.#running.map(s => s.httpSpec).filter(http => !!http)
|
|
59
164
|
}
|
|
60
|
-
}
|
|
61
165
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const keep = []
|
|
82
|
-
const next: Array<DevService> = []
|
|
83
|
-
for (const s of services) {
|
|
84
|
-
let found = false
|
|
85
|
-
for (let i = 0; i < running.length; i++) {
|
|
86
|
-
const p = running[i].s
|
|
87
|
-
if (matchingConfig(s, p)) {
|
|
88
|
-
found = true
|
|
89
|
-
keep.push(i)
|
|
90
|
-
break
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (!found) {
|
|
94
|
-
next.push(s)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
for (let i = running.length - 1; i >= 0; i--) {
|
|
98
|
-
if (!keep.includes(i)) {
|
|
99
|
-
const { s, process } = running[i]
|
|
100
|
-
if (process) {
|
|
101
|
-
stopService(s, process)
|
|
102
|
-
} else {
|
|
103
|
-
removeFromUpdating(s)
|
|
104
|
-
}
|
|
105
|
-
running.splice(i, 1)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (updating.stopping.length) {
|
|
109
|
-
for (const s of next) {
|
|
110
|
-
if (
|
|
111
|
-
!updating.starting.find(queued => matchingConfig(queued, s))
|
|
112
|
-
) {
|
|
113
|
-
updating.starting.push(s)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
updating = null
|
|
118
|
-
for (const s of next) {
|
|
119
|
-
running.push({ s, process: startService(s) })
|
|
120
|
-
}
|
|
166
|
+
shutdown = () => {
|
|
167
|
+
this.#running.forEach(s => {
|
|
168
|
+
s.kill()
|
|
169
|
+
s.removeAllListeners()
|
|
170
|
+
})
|
|
171
|
+
this.#running.length = 0
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
update(services: ResolvedDankConfig['services']) {
|
|
175
|
+
if (!services?.length) {
|
|
176
|
+
this.shutdown()
|
|
177
|
+
} else if (
|
|
178
|
+
!matchingConfigs(
|
|
179
|
+
this.#running.map(s => s.spec),
|
|
180
|
+
services,
|
|
181
|
+
)
|
|
182
|
+
) {
|
|
183
|
+
this.shutdown()
|
|
184
|
+
this.#running = this.#start(services)
|
|
121
185
|
}
|
|
122
186
|
}
|
|
187
|
+
|
|
188
|
+
#start(
|
|
189
|
+
services: NonNullable<ResolvedDankConfig['services']>,
|
|
190
|
+
): Array<ManagedService> {
|
|
191
|
+
return services.map(spec => {
|
|
192
|
+
const service = new ManagedService(spec)
|
|
193
|
+
service.on('error', (label, cause) =>
|
|
194
|
+
this.emit('error', label, cause),
|
|
195
|
+
)
|
|
196
|
+
service.on('exit', (label, code) => this.emit('exit', label, code))
|
|
197
|
+
service.on('launch', label => this.emit('launch', label))
|
|
198
|
+
service.on('stdout', (label, output) =>
|
|
199
|
+
this.emit('stdout', label, output),
|
|
200
|
+
)
|
|
201
|
+
service.on('stderr', (label, output) =>
|
|
202
|
+
this.emit('stderr', label, output),
|
|
203
|
+
)
|
|
204
|
+
return service
|
|
205
|
+
})
|
|
206
|
+
}
|
|
123
207
|
}
|
|
124
208
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
209
|
+
function matchingConfigs(
|
|
210
|
+
a: Array<DevService>,
|
|
211
|
+
b: NonNullable<ResolvedDankConfig['services']>,
|
|
212
|
+
): boolean {
|
|
213
|
+
if (a.length !== b.length) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
const crossRef = [...a]
|
|
217
|
+
for (const toFind of b) {
|
|
218
|
+
const found = crossRef.findIndex(spec => matchingConfig(spec, toFind))
|
|
219
|
+
if (found === -1) {
|
|
220
|
+
return false
|
|
221
|
+
} else {
|
|
222
|
+
crossRef.splice(found, 1)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true
|
|
129
226
|
}
|
|
130
227
|
|
|
131
228
|
function matchingConfig(a: DevService, b: DevService): boolean {
|
|
@@ -155,86 +252,6 @@ function matchingConfig(a: DevService, b: DevService): boolean {
|
|
|
155
252
|
return true
|
|
156
253
|
}
|
|
157
254
|
|
|
158
|
-
function startService(s: DevService): ChildProcess {
|
|
159
|
-
opPrint(s, 'starting')
|
|
160
|
-
const spawned = spawnService(s)
|
|
161
|
-
|
|
162
|
-
const stdoutLabel = logLabel(s, 32)
|
|
163
|
-
spawned.stdout.on('data', chunk => printChunk(stdoutLabel, chunk))
|
|
164
|
-
|
|
165
|
-
const stderrLabel = logLabel(s, 31)
|
|
166
|
-
spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk))
|
|
167
|
-
|
|
168
|
-
spawned.on('error', e => {
|
|
169
|
-
removeFromRunning(s)
|
|
170
|
-
if (e.name === 'AbortError') {
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
const cause =
|
|
174
|
-
'code' in e && e.code === 'ENOENT' ? 'program not found' : e.message
|
|
175
|
-
opPrint(s, 'error: ' + cause)
|
|
176
|
-
})
|
|
177
|
-
spawned.on('exit', () => {
|
|
178
|
-
opPrint(s, 'exited')
|
|
179
|
-
removeFromRunning(s)
|
|
180
|
-
removeFromUpdating(s)
|
|
181
|
-
})
|
|
182
|
-
return spawned
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function spawnService(s: DevService): ChildProcessWithoutNullStreams {
|
|
186
|
-
const splitCmdAndArgs = s.command.split(/\s+/)
|
|
187
|
-
const program = splitCmdAndArgs[0]
|
|
188
|
-
const args = splitCmdAndArgs.length === 1 ? [] : splitCmdAndArgs.slice(1)
|
|
189
|
-
const env = s.env ? { ...process.env, ...s.env } : undefined
|
|
190
|
-
const cwd = resolveCwd(s.cwd)
|
|
191
|
-
if (process.platform === 'win32') {
|
|
192
|
-
return spawn('cmd', ['/c', program, ...args], {
|
|
193
|
-
cwd,
|
|
194
|
-
env,
|
|
195
|
-
detached: false,
|
|
196
|
-
shell: false,
|
|
197
|
-
windowsHide: true,
|
|
198
|
-
})
|
|
199
|
-
} else {
|
|
200
|
-
return spawn(program, args, {
|
|
201
|
-
cwd,
|
|
202
|
-
env,
|
|
203
|
-
signal,
|
|
204
|
-
detached: false,
|
|
205
|
-
shell: false,
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function removeFromRunning(s: DevService) {
|
|
211
|
-
for (let i = 0; i < running.length; i++) {
|
|
212
|
-
if (matchingConfig(running[i].s, s)) {
|
|
213
|
-
running.splice(i, 1)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function removeFromUpdating(s: DevService) {
|
|
220
|
-
if (updating !== null) {
|
|
221
|
-
for (let i = 0; i < updating.stopping.length; i++) {
|
|
222
|
-
if (matchingConfig(updating.stopping[i], s)) {
|
|
223
|
-
updating.stopping.splice(i, 1)
|
|
224
|
-
if (!updating.stopping.length) {
|
|
225
|
-
updating.starting.forEach(startService)
|
|
226
|
-
updating = null
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function printChunk(label: string, c: Buffer) {
|
|
235
|
-
for (const l of parseChunk(c)) console.log(label, l)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
255
|
function parseChunk(c: Buffer): Array<string> {
|
|
239
256
|
return c
|
|
240
257
|
.toString()
|
|
@@ -242,29 +259,50 @@ function parseChunk(c: Buffer): Array<string> {
|
|
|
242
259
|
.split(/\r?\n/)
|
|
243
260
|
}
|
|
244
261
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
export function parseCommand(command: string): {
|
|
263
|
+
path: string
|
|
264
|
+
args: Array<string>
|
|
265
|
+
} {
|
|
266
|
+
command = command.trimStart()
|
|
267
|
+
const programSplitIndex = command.indexOf(' ')
|
|
268
|
+
if (programSplitIndex === -1) {
|
|
269
|
+
return { path: command.trim(), args: [] }
|
|
250
270
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
271
|
+
const path = command.substring(0, programSplitIndex)
|
|
272
|
+
const args: Array<string> = []
|
|
273
|
+
let argStart = programSplitIndex + 1
|
|
274
|
+
let withinLiteral: false | "'" | '"' = false
|
|
275
|
+
for (let i = 0; i < command.length; i++) {
|
|
276
|
+
const c = command[i]
|
|
277
|
+
if (!withinLiteral) {
|
|
278
|
+
if (c === "'" || c === '"') {
|
|
279
|
+
withinLiteral = c
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if (c === '\\') {
|
|
283
|
+
i++
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (withinLiteral) {
|
|
288
|
+
if (c === withinLiteral) {
|
|
289
|
+
withinLiteral = false
|
|
290
|
+
args.push(command.substring(argStart + 1, i))
|
|
291
|
+
argStart = i + 1
|
|
292
|
+
}
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
if (c === ' ' && i > argStart) {
|
|
296
|
+
const maybeArg = command.substring(argStart, i).trim()
|
|
297
|
+
if (maybeArg.length) {
|
|
298
|
+
args.push(maybeArg)
|
|
299
|
+
}
|
|
300
|
+
argStart = i + 1
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const maybeArg = command.substring(argStart, command.length).trim()
|
|
304
|
+
if (maybeArg.length) {
|
|
305
|
+
args.push(maybeArg)
|
|
306
|
+
}
|
|
307
|
+
return { path, args }
|
|
270
308
|
}
|
package/lib/watch.ts
CHANGED
|
@@ -1,18 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
watch as createWatch,
|
|
3
|
+
type WatchOptionsWithStringEncoding,
|
|
4
|
+
} from 'node:fs/promises'
|
|
5
|
+
|
|
6
|
+
type WatchCallback = (filename: string) => void
|
|
7
|
+
|
|
8
|
+
export async function watch(p: string, fire: WatchCallback): Promise<void>
|
|
2
9
|
|
|
3
10
|
export async function watch(
|
|
4
11
|
p: string,
|
|
5
12
|
signal: AbortSignal,
|
|
6
|
-
fire:
|
|
7
|
-
)
|
|
13
|
+
fire: WatchCallback,
|
|
14
|
+
): Promise<void>
|
|
15
|
+
|
|
16
|
+
export async function watch(
|
|
17
|
+
p: string,
|
|
18
|
+
opts: WatchOptionsWithStringEncoding,
|
|
19
|
+
fire: WatchCallback,
|
|
20
|
+
): Promise<void>
|
|
21
|
+
|
|
22
|
+
export async function watch(
|
|
23
|
+
p: string,
|
|
24
|
+
signalFireOrOpts:
|
|
25
|
+
| AbortSignal
|
|
26
|
+
| WatchCallback
|
|
27
|
+
| WatchOptionsWithStringEncoding,
|
|
28
|
+
fireOrUndefined?: WatchCallback,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
let opts: WatchOptionsWithStringEncoding | undefined
|
|
31
|
+
let fire: WatchCallback
|
|
32
|
+
if (signalFireOrOpts instanceof AbortSignal) {
|
|
33
|
+
opts = { signal: signalFireOrOpts }
|
|
34
|
+
} else if (typeof signalFireOrOpts === 'object') {
|
|
35
|
+
opts = signalFireOrOpts
|
|
36
|
+
} else {
|
|
37
|
+
fire = signalFireOrOpts
|
|
38
|
+
}
|
|
39
|
+
if (opts && typeof fireOrUndefined === 'function') {
|
|
40
|
+
fire = fireOrUndefined
|
|
41
|
+
}
|
|
8
42
|
const delayFire = 90
|
|
9
43
|
const timeout = 100
|
|
10
44
|
let changes: Record<string, number> = {}
|
|
11
45
|
try {
|
|
12
|
-
for await (const { filename } of createWatch(p, {
|
|
13
|
-
recursive: true,
|
|
14
|
-
signal,
|
|
15
|
-
})) {
|
|
46
|
+
for await (const { filename } of createWatch(p, opts)) {
|
|
16
47
|
if (filename) {
|
|
17
48
|
if (!changes[filename]) {
|
|
18
49
|
const now = Date.now()
|
package/lib_js/build.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { createBuildTag } from "./build_tag.js";
|
|
4
3
|
import { loadConfig } from "./config.js";
|
|
5
4
|
import { createGlobalDefinitions } from "./define.js";
|
|
6
5
|
import { esbuildWebpages, esbuildWorkers } from "./esbuild.js";
|
|
@@ -10,7 +9,7 @@ async function buildWebsite(c) {
|
|
|
10
9
|
if (!c) {
|
|
11
10
|
c = await loadConfig("build", process.cwd());
|
|
12
11
|
}
|
|
13
|
-
const buildTag = await
|
|
12
|
+
const buildTag = await c.buildTag();
|
|
14
13
|
console.log(c.flags.minify ? c.flags.production ? "minified production" : "minified" : "unminified", "build", buildTag, "building in ./build/dist");
|
|
15
14
|
await rm(c.dirs.buildRoot, { recursive: true, force: true });
|
|
16
15
|
await mkdir(c.dirs.buildDist, { recursive: true });
|
package/lib_js/build_tag.js
CHANGED
|
@@ -1,21 +1,81 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
-
async function createBuildTag(flags) {
|
|
2
|
+
async function createBuildTag(projectDir, flags, buildTagSource) {
|
|
3
|
+
if (typeof buildTagSource === "function") {
|
|
4
|
+
buildTagSource = await buildTagSource({ production: flags.production });
|
|
5
|
+
}
|
|
6
|
+
if (typeof buildTagSource === "undefined" || buildTagSource === null) {
|
|
7
|
+
buildTagSource = await resolveExpressionDefault(projectDir);
|
|
8
|
+
}
|
|
9
|
+
if (typeof buildTagSource !== "string") {
|
|
10
|
+
throw TypeError("DankConfig.buildTag must resolve to a string expession");
|
|
11
|
+
}
|
|
12
|
+
const params = {};
|
|
3
13
|
const now = /* @__PURE__ */ new Date();
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const paramPattern = new RegExp(/{{\s*(?<name>[a-z][A-Za-z]+)\s*}}/g);
|
|
15
|
+
let paramMatch;
|
|
16
|
+
let buildTag = buildTagSource;
|
|
17
|
+
let offset = 0;
|
|
18
|
+
while ((paramMatch = paramPattern.exec(buildTagSource)) != null) {
|
|
19
|
+
const paramName = paramMatch.groups.name.trim();
|
|
20
|
+
let paramValue;
|
|
21
|
+
if (params[paramName]) {
|
|
22
|
+
paramValue = params[paramName];
|
|
23
|
+
} else {
|
|
24
|
+
paramValue = params[paramName] = await getParamValue(projectDir, paramName, now, buildTagSource);
|
|
25
|
+
}
|
|
26
|
+
buildTag = buildTag.substring(0, paramMatch.index + offset) + paramValue + buildTag.substring(paramMatch.index + paramMatch[0].length + offset);
|
|
27
|
+
offset += paramValue.length - paramMatch[0].length;
|
|
28
|
+
}
|
|
29
|
+
const validate = /^[A-Za-z\d][A-Za-z\d-_\.]+$/;
|
|
30
|
+
if (!validate.test(buildTag)) {
|
|
31
|
+
throw Error(`build tag ${buildTag} does not pass pattern ${validate.source} validation`);
|
|
32
|
+
}
|
|
33
|
+
return buildTag;
|
|
34
|
+
}
|
|
35
|
+
async function resolveExpressionDefault(projectDir) {
|
|
36
|
+
const base = "{{ date }}-{{ timeMS }}";
|
|
37
|
+
const isGitRepo = await new Promise((res) => exec("git rev-parse --is-inside-work-tree", { cwd: projectDir }, (err) => res(!err)));
|
|
38
|
+
return isGitRepo ? base + "-{{ gitHash }}" : base;
|
|
39
|
+
}
|
|
40
|
+
async function getParamValue(projectDir, name, now, buildTagSource) {
|
|
41
|
+
switch (name) {
|
|
42
|
+
case "date":
|
|
43
|
+
return getDate(now);
|
|
44
|
+
case "gitHash":
|
|
45
|
+
try {
|
|
46
|
+
return await getGitHash(projectDir);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e === "not-repo") {
|
|
49
|
+
throw Error(`buildTag cannot use \`gitHash\` in \`${buildTagSource}\` outside of a git repository`);
|
|
50
|
+
} else {
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
case "timeMS":
|
|
55
|
+
return getTimeMS(now);
|
|
56
|
+
default:
|
|
57
|
+
throw Error(name + " is not a supported build tag param");
|
|
17
58
|
}
|
|
18
59
|
}
|
|
60
|
+
function getDate(now) {
|
|
61
|
+
return now.toISOString().substring(0, 10);
|
|
62
|
+
}
|
|
63
|
+
async function getGitHash(projectDir) {
|
|
64
|
+
return await new Promise((res, rej) => exec("git rev-parse --short HEAD", { cwd: projectDir }, (err, stdout, stderr) => {
|
|
65
|
+
if (err) {
|
|
66
|
+
if (stderr.includes("not a git repository")) {
|
|
67
|
+
rej("not-repo");
|
|
68
|
+
} else {
|
|
69
|
+
rej(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
res(stdout.trim());
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
function getTimeMS(now) {
|
|
76
|
+
const ms = now.getUTCMilliseconds() + now.getUTCSeconds() * 1e3 + now.getUTCMinutes() * 1e3 * 60 + now.getUTCHours() * 1e3 * 60 * 60;
|
|
77
|
+
return String(ms).padStart(8, "0");
|
|
78
|
+
}
|
|
19
79
|
export {
|
|
20
80
|
createBuildTag
|
|
21
81
|
};
|