@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/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 type DevServices = {
11
- http: HttpServices
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
- export type HttpServices = {
15
- running: Array<HttpService>
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
- // up to date representation of dank.config.ts services
21
- const running: Array<{ s: DevService; process: ChildProcess | null }> = []
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
- // on Windows process.kill() and AbortSignal will not kill the process spawned with cmd.exe
24
- if (process.platform === 'win32') {
25
- process.once('SIGINT', () => process.exit())
26
- process.once('exit', () => {
27
- for (const { process } of running) {
28
- if (process) {
29
- execSync(`taskkill /pid ${process.pid} /T /F`)
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
- let signal: AbortSignal
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
- // batch of services that must be stopped before starting new services
38
- let updating: null | {
39
- stopping: Array<DevService>
40
- starting: Array<DevService>
41
- } = null
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 function startDevServices(
44
- services: ResolvedDankConfig['services'],
45
- _signal: AbortSignal,
46
- ): DevServices {
47
- signal = _signal
48
- if (services?.length) {
49
- for (const s of services) {
50
- running.push({ s, process: startService(s) })
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
- return {
54
- http: {
55
- get running(): Array<HttpService> {
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
- export function updateDevServices(services: ResolvedDankConfig['services']) {
63
- if (!services?.length) {
64
- if (running.length) {
65
- if (updating === null) {
66
- updating = { stopping: [], starting: [] }
67
- }
68
- running.forEach(({ s, process }) => {
69
- if (process) {
70
- stopService(s, process)
71
- } else {
72
- removeFromUpdating(s)
73
- }
74
- })
75
- running.length = 0
76
- }
77
- } else {
78
- if (updating === null) {
79
- updating = { stopping: [], starting: [] }
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 stopService(s: DevService, process: ChildProcess) {
126
- opPrint(s, 'stopping')
127
- updating!.stopping.push(s)
128
- process.kill()
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 resolveCwd(p?: string): string | undefined {
246
- if (!p || isAbsolute(p)) {
247
- return p
248
- } else {
249
- return resolve(process.cwd(), p)
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
- function opPrint(s: DevService, msg: string) {
254
- console.log(opLabel(s), msg)
255
- }
256
-
257
- function opLabel(s: DevService) {
258
- return `\`${s.cwd ? s.cwd + ' ' : ''}${s.command}\``
259
- }
260
-
261
- function logLabel(s: DevService, ansiColor: number): string {
262
- s.cwd = !s.cwd
263
- ? './'
264
- : s.cwd.startsWith('/')
265
- ? `/.../${basename(s.cwd)}`
266
- : s.cwd.startsWith('.')
267
- ? s.cwd
268
- : `./${s.cwd}`
269
- return `\u001b[${ansiColor}m[\u001b[1m${s.command}\u001b[22m \u001b[2;3m${s.cwd}\u001b[22;23m]\u001b[0m`
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 { watch as createWatch } from 'node:fs/promises'
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: (filename: string) => void,
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 createBuildTag(c.flags);
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 });
@@ -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 ms = now.getUTCMilliseconds() + now.getUTCSeconds() * 1e3 + now.getUTCMinutes() * 1e3 * 60 + now.getUTCHours() * 1e3 * 60 * 60;
5
- const date = now.toISOString().substring(0, 10);
6
- const time = String(ms).padStart(8, "0");
7
- const when = `${date}-${time}`;
8
- if (flags.production) {
9
- const gitHash = await new Promise((res, rej) => exec("git rev-parse --short HEAD", (err, stdout) => {
10
- if (err)
11
- rej(err);
12
- res(stdout.trim());
13
- }));
14
- return `${when}-${gitHash}`;
15
- } else {
16
- return when;
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
  };