@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 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.ts'
4
3
  import { loadConfig, type ResolvedDankConfig } from './config.ts'
5
4
  import { type DefineDankGlobal, createGlobalDefinitions } from './define.ts'
6
5
  import type { DankDirectories } from './dirs.ts'
@@ -14,7 +13,7 @@ export async function buildWebsite(
14
13
  if (!c) {
15
14
  c = await loadConfig('build', process.cwd())
16
15
  }
17
- const buildTag = await createBuildTag(c.flags)
16
+ const buildTag = await c.buildTag()
18
17
  console.log(
19
18
  c.flags.minify
20
19
  ? c.flags.production
package/lib/build_tag.ts CHANGED
@@ -1,25 +1,129 @@
1
1
  import { exec } from 'node:child_process'
2
+ import type { DankConfig } from './dank.ts'
2
3
  import type { DankFlags } from './flags.ts'
3
4
 
4
- export async function createBuildTag(flags: DankFlags): Promise<string> {
5
+ export async function createBuildTag(
6
+ projectDir: string,
7
+ flags: DankFlags,
8
+ buildTagSource?: DankConfig['buildTag'],
9
+ ): Promise<string> {
10
+ if (typeof buildTagSource === 'function') {
11
+ buildTagSource = await buildTagSource({ production: flags.production })
12
+ }
13
+ if (typeof buildTagSource === 'undefined' || buildTagSource === null) {
14
+ buildTagSource = await resolveExpressionDefault(projectDir)
15
+ }
16
+ if (typeof buildTagSource !== 'string') {
17
+ throw TypeError(
18
+ 'DankConfig.buildTag must resolve to a string expession',
19
+ )
20
+ }
21
+ const params: BuildTagParams = {}
5
22
  const now = new Date()
23
+ const paramPattern = new RegExp(/{{\s*(?<name>[a-z][A-Za-z]+)\s*}}/g)
24
+ let paramMatch: RegExpExecArray | null
25
+ let buildTag = buildTagSource
26
+ let offset = 0
27
+ while ((paramMatch = paramPattern.exec(buildTagSource)) != null) {
28
+ const paramName = paramMatch.groups!.name.trim() as keyof BuildTagParams
29
+ let paramValue: string
30
+ if (params[paramName]) {
31
+ paramValue = params[paramName]
32
+ } else {
33
+ paramValue = params[paramName] = await getParamValue(
34
+ projectDir,
35
+ paramName,
36
+ now,
37
+ buildTagSource,
38
+ )
39
+ }
40
+ buildTag =
41
+ buildTag.substring(0, paramMatch.index + offset) +
42
+ paramValue +
43
+ buildTag.substring(paramMatch.index + paramMatch[0].length + offset)
44
+ offset += paramValue.length - paramMatch[0].length
45
+ }
46
+ const validate = /^[A-Za-z\d][A-Za-z\d-_\.]+$/
47
+ if (!validate.test(buildTag)) {
48
+ throw Error(
49
+ `build tag ${buildTag} does not pass pattern ${validate.source} validation`,
50
+ )
51
+ }
52
+ return buildTag
53
+ }
54
+
55
+ async function resolveExpressionDefault(projectDir: string): Promise<string> {
56
+ const base = '{{ date }}-{{ timeMS }}'
57
+ const isGitRepo = await new Promise(res =>
58
+ exec('git rev-parse --is-inside-work-tree', { cwd: projectDir }, err =>
59
+ res(!err),
60
+ ),
61
+ )
62
+ return isGitRepo ? base + '-{{ gitHash }}' : base
63
+ }
64
+
65
+ type BuildTagParams = {
66
+ date?: string
67
+ gitHash?: string
68
+ timeMS?: string
69
+ }
70
+
71
+ async function getParamValue(
72
+ projectDir: string,
73
+ name: keyof BuildTagParams,
74
+ now: Date,
75
+ buildTagSource: string,
76
+ ): Promise<string> {
77
+ switch (name) {
78
+ case 'date':
79
+ return getDate(now)
80
+ case 'gitHash':
81
+ try {
82
+ return await getGitHash(projectDir)
83
+ } catch (e) {
84
+ if (e === 'not-repo') {
85
+ throw Error(
86
+ `buildTag cannot use \`gitHash\` in \`${buildTagSource}\` outside of a git repository`,
87
+ )
88
+ } else {
89
+ throw e
90
+ }
91
+ }
92
+ case 'timeMS':
93
+ return getTimeMS(now)
94
+ default:
95
+ throw Error(name + ' is not a supported build tag param')
96
+ }
97
+ }
98
+
99
+ function getDate(now: Date): string {
100
+ return now.toISOString().substring(0, 10)
101
+ }
102
+
103
+ async function getGitHash(projectDir: string): Promise<string> {
104
+ return await new Promise((res, rej) =>
105
+ exec(
106
+ 'git rev-parse --short HEAD',
107
+ { cwd: projectDir },
108
+ (err, stdout, stderr) => {
109
+ if (err) {
110
+ if (stderr.includes('not a git repository')) {
111
+ rej('not-repo')
112
+ } else {
113
+ rej(err)
114
+ }
115
+ }
116
+ res(stdout.trim())
117
+ },
118
+ ),
119
+ )
120
+ }
121
+
122
+ function getTimeMS(now: Date): string {
6
123
  const ms =
7
124
  now.getUTCMilliseconds() +
8
125
  now.getUTCSeconds() * 1000 +
9
126
  now.getUTCMinutes() * 1000 * 60 +
10
127
  now.getUTCHours() * 1000 * 60 * 60
11
- const date = now.toISOString().substring(0, 10)
12
- const time = String(ms).padStart(8, '0')
13
- const when = `${date}-${time}`
14
- if (flags.production) {
15
- const gitHash = await new Promise((res, rej) =>
16
- exec('git rev-parse --short HEAD', (err, stdout) => {
17
- if (err) rej(err)
18
- res(stdout.trim())
19
- }),
20
- )
21
- return `${when}-${gitHash}`
22
- } else {
23
- return when
24
- }
128
+ return String(ms).padStart(8, '0')
25
129
  }
package/lib/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, resolve } from 'node:path'
2
+ import { createBuildTag } from './build_tag.ts'
2
3
  import type {
3
4
  DankConfig,
4
5
  DankDetails,
@@ -34,6 +35,8 @@ export type ResolvedDankConfig = {
34
35
  get devPages(): Readonly<DankConfig['devPages']>
35
36
  get services(): Readonly<DankConfig['services']>
36
37
 
38
+ buildTag(): Promise<string>
39
+
37
40
  reload(): Promise<void>
38
41
  }
39
42
 
@@ -60,6 +63,7 @@ export async function loadConfig(
60
63
  }
61
64
 
62
65
  class DankConfigInternal implements ResolvedDankConfig {
66
+ #buildTag: DankConfig['buildTag']
63
67
  #dirs: Readonly<DankDirectories>
64
68
  #flags: Readonly<DankFlags>
65
69
  #mode: 'build' | 'serve'
@@ -119,11 +123,20 @@ class DankConfigInternal implements ResolvedDankConfig {
119
123
  return this.#services
120
124
  }
121
125
 
126
+ async buildTag(): Promise<string> {
127
+ return await createBuildTag(
128
+ this.#dirs.projectRootAbs,
129
+ this.#flags,
130
+ this.#buildTag,
131
+ )
132
+ }
133
+
122
134
  async reload() {
123
135
  const userConfig = await resolveConfig(
124
136
  this.#modulePath,
125
137
  resolveDankDetails(this.#mode, this.#flags),
126
138
  )
139
+ this.#buildTag = userConfig.buildTag
127
140
  this.#dankPort = resolveDankPort(this.#flags, userConfig)
128
141
  this.#esbuildPort = resolveEsbuildPort(this.#flags, userConfig)
129
142
  this.#esbuild = Object.freeze(userConfig.esbuild)
@@ -173,6 +186,7 @@ function resolveDankDetails(
173
186
  function validateDankConfig(c: Partial<DankConfig>) {
174
187
  try {
175
188
  validatePorts(c)
189
+ validateBuildTag(c.buildTag)
176
190
  validatePages(c.pages)
177
191
  validateDevPages(c.devPages)
178
192
  validateDevServices(c.services)
@@ -202,6 +216,20 @@ function validatePorts(c: Partial<DankConfig>) {
202
216
  }
203
217
  }
204
218
 
219
+ function validateBuildTag(buildTag: DankConfig['buildTag']) {
220
+ if (buildTag === null) {
221
+ return
222
+ }
223
+ switch (typeof buildTag) {
224
+ case 'undefined':
225
+ case 'string':
226
+ case 'function':
227
+ return
228
+ default:
229
+ throw Error('DankConfig.buildTag must be a string or function')
230
+ }
231
+ }
232
+
205
233
  function validateEsbuildConfig(esbuild?: EsbuildConfig) {
206
234
  if (esbuild?.loaders !== null && typeof esbuild?.loaders !== 'undefined') {
207
235
  if (typeof esbuild.loaders !== 'object') {
package/lib/dank.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { Plugin as EsbuildPlugin } from 'esbuild'
2
2
 
3
3
  export type DankConfig = {
4
- // used for releases and service worker caching
5
- // buildTag?: (() => Promise<string> | string) | string
4
+ // used for service worker caching
5
+ buildTag?: string | BuildTagBuilder
6
6
 
7
7
  // customize esbuild configs
8
8
  esbuild?: EsbuildConfig
@@ -26,6 +26,14 @@ export type DankConfig = {
26
26
  services?: Array<DevService>
27
27
  }
28
28
 
29
+ export type BuildTagParams = {
30
+ production: boolean
31
+ }
32
+
33
+ export type BuildTagBuilder = (
34
+ build: BuildTagParams,
35
+ ) => Promise<string> | string
36
+
29
37
  // extend an html entrypoint with url rewriting similar to cdn configurations
30
38
  // after trying all webpage, bundle and asset paths, mapping patterns
31
39
  // will be tested in the alphabetical order of the webpage paths
package/lib/dirs.ts CHANGED
@@ -7,8 +7,7 @@ export type DankDirectories = {
7
7
  buildWatch: string
8
8
  buildDist: string
9
9
  pages: string
10
- pagesResolved: string
11
- projectResolved: string
10
+ pagesAbs: string
12
11
  projectRootAbs: string
13
12
  public: string
14
13
  }
@@ -17,18 +16,18 @@ export async function defaultProjectDirs(
17
16
  projectRootAbs: string,
18
17
  ): Promise<Readonly<DankDirectories>> {
19
18
  if (!isAbsolute(projectRootAbs)) {
20
- throw Error()
19
+ throw Error('must use an absolute project root path')
20
+ }
21
+ if ((await realpath(projectRootAbs)) !== projectRootAbs) {
22
+ throw Error('must use a real project root path')
21
23
  }
22
- const projectResolved = await realpath(projectRootAbs)
23
24
  const pages = 'pages'
24
- const pagesResolved = join(projectResolved, pages)
25
25
  return Object.freeze({
26
26
  buildRoot: 'build',
27
27
  buildDist: join('build', 'dist'),
28
28
  buildWatch: join('build', 'watch'),
29
29
  pages,
30
- pagesResolved,
31
- projectResolved,
30
+ pagesAbs: join(projectRootAbs, pages),
32
31
  projectRootAbs,
33
32
  public: 'public',
34
33
  })
@@ -63,8 +62,8 @@ export class Resolver {
63
62
 
64
63
  // `p` is expected to be a relative path resolvable from the project dir
65
64
  isProjectSubpathInPagesDir(p: string): boolean {
66
- return resolve(join(this.#dirs.projectResolved, p)).startsWith(
67
- this.#dirs.pagesResolved,
65
+ return resolve(join(this.#dirs.projectRootAbs, p)).startsWith(
66
+ this.#dirs.pagesAbs,
68
67
  )
69
68
  }
70
69
 
package/lib/http.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  WebsiteManifest,
19
19
  WebsiteRegistry,
20
20
  } from './registry.ts'
21
- import type { HttpServices } from './services.ts'
21
+ import type { DevServices } from './services.ts'
22
22
 
23
23
  export type FrontendFetcher = (
24
24
  url: URL,
@@ -33,7 +33,7 @@ export function startWebServer(
33
33
  dirs: DankDirectories,
34
34
  urlRewriteProvider: UrlRewriteProvider,
35
35
  frontendFetcher: FrontendFetcher,
36
- httpServices: HttpServices,
36
+ devServices: DevServices,
37
37
  ) {
38
38
  const serverAddress = 'http://localhost:' + port
39
39
  const handler = (req: IncomingMessage, res: ServerResponse) => {
@@ -47,7 +47,7 @@ export function startWebServer(
47
47
  req,
48
48
  url,
49
49
  headers,
50
- httpServices,
50
+ devServices,
51
51
  flags,
52
52
  dirs,
53
53
  urlRewriteProvider,
@@ -69,7 +69,7 @@ async function onNotFound(
69
69
  req: IncomingMessage,
70
70
  url: URL,
71
71
  headers: Headers,
72
- httpServices: HttpServices,
72
+ devServices: DevServices,
73
73
  flags: DankFlags,
74
74
  dirs: DankDirectories,
75
75
  urlRewriteProvider: UrlRewriteProvider,
@@ -87,7 +87,7 @@ async function onNotFound(
87
87
  return
88
88
  }
89
89
  }
90
- const fetchResponse = await tryHttpServices(req, url, headers, httpServices)
90
+ const fetchResponse = await tryHttpServices(req, url, headers, devServices)
91
91
  if (fetchResponse) {
92
92
  sendFetchResponse(res, fetchResponse)
93
93
  } else {
@@ -131,14 +131,13 @@ async function tryHttpServices(
131
131
  req: IncomingMessage,
132
132
  url: URL,
133
133
  headers: Headers,
134
- httpServices: HttpServices,
134
+ devServices: DevServices,
135
135
  ): Promise<Response | null> {
136
136
  if (url.pathname.startsWith('/.well-known/')) {
137
137
  return null
138
138
  }
139
139
  const body = await collectReqBody(req)
140
- const { running } = httpServices
141
- for (const httpService of running) {
140
+ for (const httpService of devServices.httpServices) {
142
141
  const proxyUrl = new URL(url)
143
142
  proxyUrl.port = `${httpService.port}`
144
143
  try {
@@ -198,7 +197,7 @@ export function createBuiltDistFilesFetcher(
198
197
  if (manifest.pageUrls.has(url.pathname)) {
199
198
  streamFile(
200
199
  join(
201
- dirs.projectResolved,
200
+ dirs.projectRootAbs,
202
201
  dirs.buildDist,
203
202
  url.pathname,
204
203
  'index.html',
@@ -207,7 +206,7 @@ export function createBuiltDistFilesFetcher(
207
206
  )
208
207
  } else if (manifest.files.has(url.pathname)) {
209
208
  streamFile(
210
- join(dirs.projectResolved, dirs.buildDist, url.pathname),
209
+ join(dirs.projectRootAbs, dirs.buildDist, url.pathname),
211
210
  res,
212
211
  )
213
212
  } else {
package/lib/serve.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  startWebServer,
14
14
  } from './http.ts'
15
15
  import { WebsiteRegistry, type UrlRewrite } from './registry.ts'
16
- import { startDevServices, updateDevServices } from './services.ts'
16
+ import { DevServices, type ManagedServiceLabel } from './services.ts'
17
17
  import { watch } from './watch.ts'
18
18
 
19
19
  let c: ResolvedDankConfig
@@ -21,20 +21,18 @@ let c: ResolvedDankConfig
21
21
  export async function serveWebsite(): Promise<never> {
22
22
  c = await loadConfig('serve', process.cwd())
23
23
  await rm(c.dirs.buildRoot, { force: true, recursive: true })
24
- const abortController = new AbortController()
25
- process.once('exit', () => abortController.abort())
26
24
  if (c.flags.preview) {
27
- await startPreviewMode(abortController.signal)
25
+ await startPreviewMode()
28
26
  } else {
29
- await startDevMode(abortController.signal)
27
+ await startDevMode()
30
28
  }
31
29
  return new Promise(() => {})
32
30
  }
33
31
 
34
- async function startPreviewMode(signal: AbortSignal) {
32
+ async function startPreviewMode() {
35
33
  const manifest = await buildWebsite(c)
36
34
  const frontend = createBuiltDistFilesFetcher(c.dirs, manifest)
37
- const devServices = startDevServices(c.services, signal)
35
+ const devServices = launchDevServices()
38
36
  const urlRewrites: Array<UrlRewrite> = Object.keys(c.pages)
39
37
  .sort()
40
38
  .map(url => {
@@ -50,8 +48,17 @@ async function startPreviewMode(signal: AbortSignal) {
50
48
  c.dirs,
51
49
  { urlRewrites },
52
50
  frontend,
53
- devServices.http,
51
+ devServices,
54
52
  )
53
+ const controller = new AbortController()
54
+ watch('dank.config.ts', controller.signal, async filename => {
55
+ console.log(filename, 'was updated!')
56
+ console.log(
57
+ 'config updates are not hot reloaded during `dank serve --preview`',
58
+ )
59
+ console.log('restart DANK to reload configuration')
60
+ controller.abort()
61
+ })
55
62
  }
56
63
 
57
64
  type BuildContextState =
@@ -61,12 +68,12 @@ type BuildContextState =
61
68
  | 'disposing'
62
69
  | null
63
70
 
64
- async function startDevMode(signal: AbortSignal) {
71
+ async function startDevMode() {
65
72
  const registry = new WebsiteRegistry(c)
66
73
  await mkdir(c.dirs.buildWatch, { recursive: true })
67
74
  let buildContext: BuildContextState = null
68
75
 
69
- watch('dank.config.ts', signal, async filename => {
76
+ watch('dank.config.ts', async filename => {
70
77
  LOG({
71
78
  realm: 'serve',
72
79
  message: 'config watch event',
@@ -80,10 +87,10 @@ async function startDevMode(signal: AbortSignal) {
80
87
  return
81
88
  }
82
89
  registry.configSync()
83
- updateDevServices(c.services)
90
+ devServices.update(c.services)
84
91
  })
85
92
 
86
- watch(c.dirs.pages, signal, filename => {
93
+ watch(c.dirs.pages, { recursive: true }, filename => {
87
94
  LOG({
88
95
  realm: 'serve',
89
96
  message: 'pages dir watch event',
@@ -163,15 +170,8 @@ async function startDevMode(signal: AbortSignal) {
163
170
  resetBuildContext()
164
171
 
165
172
  const frontend = createDevServeFilesFetcher(c.esbuildPort, c.dirs, registry)
166
- const devServices = startDevServices(c.services, signal)
167
- startWebServer(
168
- c.dankPort,
169
- c.flags,
170
- c.dirs,
171
- registry,
172
- frontend,
173
- devServices.http,
174
- )
173
+ const devServices = launchDevServices()
174
+ startWebServer(c.dankPort, c.flags, c.dirs, registry, frontend, devServices)
175
175
  }
176
176
 
177
177
  async function startEsbuildWatch(
@@ -220,3 +220,47 @@ async function writeHtml(html: HtmlEntrypoint, output: string) {
220
220
  })
221
221
  await writeFile(path, output)
222
222
  }
223
+
224
+ function launchDevServices(): DevServices {
225
+ const services = new DevServices(c.services)
226
+ services.on('error', (label, cause) =>
227
+ console.log(formatServiceLabel(label), 'errored:', cause),
228
+ )
229
+ services.on('exit', (label, code) => {
230
+ if (code) {
231
+ console.log(formatServiceLabel(label), 'exited', code)
232
+ } else {
233
+ console.log(formatServiceLabel(label), 'exited')
234
+ }
235
+ })
236
+ services.on('launch', label =>
237
+ console.log(formatServiceLabel(label), 'starting'),
238
+ )
239
+ services.on('stdout', (label, output) =>
240
+ printServiceOutput(label, 32, output),
241
+ )
242
+ services.on('stderr', (label, output) =>
243
+ printServiceOutput(label, 31, output),
244
+ )
245
+ return services
246
+ }
247
+
248
+ function formatServiceLabel(label: ManagedServiceLabel): string {
249
+ return `| \u001b[2m${label.cwd}\u001b[22m ${label.command} |`
250
+ }
251
+
252
+ function formatServiceOutputLabel(
253
+ label: ManagedServiceLabel,
254
+ color: 31 | 32,
255
+ ): string {
256
+ return `\u001b[${color}m${formatServiceLabel(label)}\u001b[39m`
257
+ }
258
+
259
+ function printServiceOutput(
260
+ label: ManagedServiceLabel,
261
+ color: 31 | 32,
262
+ output: Array<string>,
263
+ ) {
264
+ const formattedLabel = formatServiceOutputLabel(label, color)
265
+ for (const line of output) console.log(formattedLabel, line)
266
+ }