@eighty4/dank 0.0.4-1 → 0.0.4-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/http.ts CHANGED
@@ -10,8 +10,14 @@ import {
10
10
  import { extname, join } from 'node:path'
11
11
  import { Readable } from 'node:stream'
12
12
  import mime from 'mime'
13
- import type { DankServe } from './flags.ts'
14
- import type { WebsiteManifest } from './metadata.ts'
13
+ import type { DankDirectories } from './dirs.ts'
14
+ import type { DankFlags } from './flags.ts'
15
+ import type {
16
+ UrlRewrite,
17
+ UrlRewriteProvider,
18
+ WebsiteManifest,
19
+ WebsiteRegistry,
20
+ } from './registry.ts'
15
21
  import type { HttpServices } from './services.ts'
16
22
 
17
23
  export type FrontendFetcher = (
@@ -21,25 +27,15 @@ export type FrontendFetcher = (
21
27
  notFound: () => void,
22
28
  ) => void
23
29
 
24
- // state needed to eval url rewriting after FrontendFetcher and before HttpServices
25
- export type PageRouteState = {
26
- // urls of html entrypoints
27
- urls: Array<string>
28
- urlRewrites: Array<UrlRewrite>
29
- }
30
-
31
- export type UrlRewrite = {
32
- pattern: RegExp
33
- url: string
34
- }
35
-
36
30
  export function startWebServer(
37
- serve: DankServe,
31
+ port: number,
32
+ flags: DankFlags,
33
+ dirs: DankDirectories,
34
+ urlRewriteProvider: UrlRewriteProvider,
38
35
  frontendFetcher: FrontendFetcher,
39
36
  httpServices: HttpServices,
40
- pageRoutes: PageRouteState,
41
37
  ) {
42
- const serverAddress = 'http://localhost:' + serve.dankPort
38
+ const serverAddress = 'http://localhost:' + port
43
39
  const handler = (req: IncomingMessage, res: ServerResponse) => {
44
40
  if (!req.url || !req.method) {
45
41
  res.end()
@@ -52,19 +48,20 @@ export function startWebServer(
52
48
  url,
53
49
  headers,
54
50
  httpServices,
55
- pageRoutes,
56
- serve,
51
+ flags,
52
+ dirs,
53
+ urlRewriteProvider,
57
54
  res,
58
55
  ),
59
56
  )
60
57
  }
61
58
  }
62
- createServer(serve.logHttp ? createLogWrapper(handler) : handler).listen(
63
- serve.dankPort,
59
+ createServer(flags.logHttp ? createLogWrapper(handler) : handler).listen(
60
+ port,
64
61
  )
65
62
  console.log(
66
- serve.preview ? 'preview' : 'dev',
67
- `server is live at http://127.0.0.1:${serve.dankPort}`,
63
+ flags.preview ? 'preview' : 'dev',
64
+ `server is live at http://127.0.0.1:${port}`,
68
65
  )
69
66
  }
70
67
 
@@ -73,12 +70,18 @@ async function onNotFound(
73
70
  url: URL,
74
71
  headers: Headers,
75
72
  httpServices: HttpServices,
76
- pageRoutes: PageRouteState,
77
- serve: DankServe,
73
+ flags: DankFlags,
74
+ dirs: DankDirectories,
75
+ urlRewriteProvider: UrlRewriteProvider,
78
76
  res: ServerResponse,
79
77
  ) {
80
78
  if (req.method === 'GET' && extname(url.pathname) === '') {
81
- const urlRewrite = tryUrlRewrites(url, pageRoutes, serve)
79
+ const urlRewrite = tryUrlRewrites(
80
+ flags,
81
+ dirs,
82
+ urlRewriteProvider.urlRewrites,
83
+ url,
84
+ )
82
85
  if (urlRewrite) {
83
86
  streamFile(urlRewrite, res)
84
87
  return
@@ -107,15 +110,20 @@ async function sendFetchResponse(res: ServerResponse, fetchResponse: Response) {
107
110
  }
108
111
 
109
112
  function tryUrlRewrites(
113
+ flags: DankFlags,
114
+ dirs: DankDirectories,
115
+ urlRewrites: Array<UrlRewrite>,
110
116
  url: URL,
111
- pageRoutes: PageRouteState,
112
- serve: DankServe,
113
117
  ): string | null {
114
- const urlRewrite = pageRoutes.urlRewrites.find(urlRewrite =>
118
+ const urlRewrite = urlRewrites.find(urlRewrite =>
115
119
  urlRewrite.pattern.test(url.pathname),
116
120
  )
117
121
  return urlRewrite
118
- ? join(serve.dirs.buildWatch, urlRewrite.url, 'index.html')
122
+ ? join(
123
+ flags.preview ? dirs.buildDist : dirs.buildWatch,
124
+ urlRewrite.url,
125
+ 'index.html',
126
+ )
119
127
  : null
120
128
  }
121
129
 
@@ -178,7 +186,7 @@ function createLogWrapper(handler: RequestListener): RequestListener {
178
186
  }
179
187
 
180
188
  export function createBuiltDistFilesFetcher(
181
- dir: string,
189
+ dirs: DankDirectories,
182
190
  manifest: WebsiteManifest,
183
191
  ): FrontendFetcher {
184
192
  return (
@@ -188,34 +196,42 @@ export function createBuiltDistFilesFetcher(
188
196
  notFound: () => void,
189
197
  ) => {
190
198
  if (manifest.pageUrls.has(url.pathname)) {
191
- streamFile(join(dir, url.pathname, 'index.html'), res)
199
+ streamFile(
200
+ join(
201
+ dirs.projectResolved,
202
+ dirs.buildDist,
203
+ url.pathname,
204
+ 'index.html',
205
+ ),
206
+ res,
207
+ )
192
208
  } else if (manifest.files.has(url.pathname)) {
193
- streamFile(join(dir, url.pathname), res)
209
+ streamFile(
210
+ join(dirs.projectResolved, dirs.buildDist, url.pathname),
211
+ res,
212
+ )
194
213
  } else {
195
214
  notFound()
196
215
  }
197
216
  }
198
217
  }
199
218
 
200
- // todo replace PageRouteState with WebsiteRegistry
201
219
  export function createDevServeFilesFetcher(
202
- pageRoutes: PageRouteState,
203
- serve: DankServe,
220
+ esbuildPort: number,
221
+ dirs: DankDirectories,
222
+ registry: WebsiteRegistry,
204
223
  ): FrontendFetcher {
205
- const proxyAddress = 'http://127.0.0.1:' + serve.esbuildPort
224
+ const proxyAddress = 'http://127.0.0.1:' + esbuildPort
206
225
  return (
207
226
  url: URL,
208
227
  _headers: Headers,
209
228
  res: ServerResponse,
210
229
  notFound: () => void,
211
230
  ) => {
212
- if (pageRoutes.urls.includes(url.pathname)) {
213
- streamFile(
214
- join(serve.dirs.buildWatch, url.pathname, 'index.html'),
215
- res,
216
- )
231
+ if (registry.pageUrls.includes(url.pathname)) {
232
+ streamFile(join(dirs.buildWatch, url.pathname, 'index.html'), res)
217
233
  } else {
218
- const maybePublicPath = join(serve.dirs.public, url.pathname)
234
+ const maybePublicPath = join(dirs.public, url.pathname)
219
235
  exists(maybePublicPath).then(fromPublic => {
220
236
  if (fromPublic) {
221
237
  streamFile(maybePublicPath, res)
package/lib/public.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { copyFile, mkdir, readdir, stat } from 'node:fs/promises'
2
2
  import { platform } from 'node:os'
3
3
  import { join } from 'node:path'
4
- import type { DankBuild } from './flags.ts'
4
+ import type { DankDirectories } from './dirs.ts'
5
5
 
6
6
  export async function copyAssets(
7
- build: DankBuild,
7
+ dirs: DankDirectories,
8
8
  ): Promise<Array<string> | null> {
9
9
  try {
10
- const stats = await stat(build.dirs.public)
10
+ const stats = await stat(dirs.public)
11
11
  if (stats.isDirectory()) {
12
- await mkdir(build.dirs.buildDist, { recursive: true })
13
- return await recursiveCopyAssets(build)
12
+ await mkdir(dirs.buildDist, { recursive: true })
13
+ return await recursiveCopyAssets(dirs)
14
14
  } else {
15
15
  throw Error('./public cannot be a file')
16
16
  }
@@ -22,13 +22,13 @@ export async function copyAssets(
22
22
  const IGNORE = platform() === 'darwin' ? ['.DS_Store'] : []
23
23
 
24
24
  async function recursiveCopyAssets(
25
- build: DankBuild,
25
+ dirs: DankDirectories,
26
26
  dir: string = '',
27
27
  ): Promise<Array<string>> {
28
28
  const copied: Array<string> = []
29
- const to = join(build.dirs.buildDist, dir)
29
+ const to = join(dirs.buildDist, dir)
30
30
  let madeDir = dir === ''
31
- const listingDir = join(build.dirs.public, dir)
31
+ const listingDir = join(dirs.public, dir)
32
32
  for (const p of await readdir(listingDir)) {
33
33
  if (IGNORE.includes(p)) {
34
34
  continue
@@ -36,10 +36,10 @@ async function recursiveCopyAssets(
36
36
  try {
37
37
  const stats = await stat(join(listingDir, p))
38
38
  if (stats.isDirectory()) {
39
- copied.push(...(await recursiveCopyAssets(build, join(dir, p))))
39
+ copied.push(...(await recursiveCopyAssets(dirs, join(dir, p))))
40
40
  } else {
41
41
  if (!madeDir) {
42
- await mkdir(join(build.dirs.buildDist, dir), {
42
+ await mkdir(join(dirs.buildDist, dir), {
43
43
  recursive: true,
44
44
  })
45
45
  madeDir = true
@@ -1,52 +1,13 @@
1
1
  import EventEmitter from 'node:events'
2
2
  import { writeFile } from 'node:fs/promises'
3
- import { dirname, join, resolve } from 'node:path'
3
+ import { join } from 'node:path'
4
4
  import type { BuildResult } from 'esbuild'
5
+ import type { ResolvedDankConfig } from './config.ts'
6
+ import { LOG } from './developer.ts'
7
+ import { Resolver, type DankDirectories } from './dirs.ts'
5
8
  import type { EntryPoint } from './esbuild.ts'
6
- import type { DankBuild, ProjectDirs } from './flags.ts'
7
-
8
- export type ResolveError = 'outofbounds'
9
-
10
- export type Resolver = {
11
- // `p` is expected to be a relative path resolvable from the project dir
12
- isProjectSubpathInPagesDir(p: string): boolean
13
-
14
- // `p` is expected to be a relative path resolvable from the pages dir
15
- isPagesSubpathInPagesDir(p: string): boolean
16
-
17
- // resolve a pages subpath from a resource within the pages directory by a relative href
18
- // `from` is expected to be a pages resource fs path starting with `pages/` and ending with filename
19
- // the result will be a pages subpath and will not have the pages dir prefix
20
- // returns 'outofbounds' if the relative path does not resolve to a file within the pages dir
21
- resolveHrefInPagesDir(from: string, href: string): string | ResolveError
22
- }
23
-
24
- class ResolverImpl implements Resolver {
25
- #dirs: ProjectDirs
26
-
27
- constructor(dirs: ProjectDirs) {
28
- this.#dirs = dirs
29
- }
30
-
31
- isProjectSubpathInPagesDir(p: string): boolean {
32
- return resolve(join(this.#dirs.projectResolved, p)).startsWith(
33
- this.#dirs.pagesResolved,
34
- )
35
- }
36
-
37
- isPagesSubpathInPagesDir(p: string): boolean {
38
- return this.isProjectSubpathInPagesDir(join(this.#dirs.pages, p))
39
- }
40
-
41
- resolveHrefInPagesDir(from: string, href: string): string | ResolveError {
42
- const p = join(dirname(from), href)
43
- if (this.isProjectSubpathInPagesDir(p)) {
44
- return p
45
- } else {
46
- return 'outofbounds'
47
- }
48
- }
49
- }
9
+ import { HtmlEntrypoint } from './html.ts'
10
+ import type { PageMapping } from './dank.ts'
50
11
 
51
12
  // summary of a website build
52
13
  export type WebsiteManifest = {
@@ -75,31 +36,55 @@ type WorkerManifest = {
75
36
  // path to bundled entrypoint dependent on `clientScript`
76
37
  dependentEntryPoint: string
77
38
  workerEntryPoint: string
39
+ workerCtor: 'Worker' | 'SharedWorker'
78
40
  workerUrl: string
79
41
  workerUrlPlaceholder: string
80
42
  }
81
43
 
82
44
  export type WebsiteRegistryEvents = {
45
+ entrypoints: []
46
+ webpage: [entrypoint: HtmlEntrypoint]
83
47
  workers: []
84
48
  }
85
49
 
50
+ type WebpageRegistration = {
51
+ pageUrl: `/${string}`
52
+ fsPath: string
53
+ html: HtmlEntrypoint
54
+ bundles: Array<EntryPoint>
55
+ urlRewrite?: UrlRewrite
56
+ }
57
+
58
+ export type UrlRewrite = {
59
+ pattern: RegExp
60
+ url: string
61
+ }
62
+
63
+ export type UrlRewriteProvider = {
64
+ urlRewrites: Array<UrlRewrite>
65
+ }
66
+
86
67
  // manages website resources during `dank build` and `dank serve`
87
68
  export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
88
- #build: DankBuild
89
- // paths of bundled esbuild outputs
69
+ // paths of bundled esbuild outputs, as built by esbuild
90
70
  #bundles: Set<string> = new Set()
71
+ #c: ResolvedDankConfig
91
72
  // public dir assets
92
73
  #copiedAssets: Set<string> | null = null
93
74
  // map of entrypoints to their output path
94
75
  #entrypointHrefs: Record<string, string | null> = {}
95
- #pageUrls: Array<string> = []
96
- #resolver: Resolver
76
+ #pages: Record<`/${string}`, WebpageRegistration> = {}
77
+ readonly #resolver: Resolver
97
78
  #workers: Array<WorkerManifest> | null = null
98
79
 
99
- constructor(build: DankBuild) {
80
+ constructor(config: ResolvedDankConfig) {
100
81
  super()
101
- this.#build = build
102
- this.#resolver = new ResolverImpl(build.dirs)
82
+ this.#c = config
83
+ this.#resolver = new Resolver(config.dirs)
84
+ }
85
+
86
+ get config(): ResolvedDankConfig {
87
+ return this.#c
103
88
  }
104
89
 
105
90
  set copiedAssets(copiedAssets: Array<string> | null) {
@@ -107,29 +92,88 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
107
92
  copiedAssets === null ? null : new Set(copiedAssets)
108
93
  }
109
94
 
110
- set pageUrls(pageUrls: Array<string>) {
111
- this.#pageUrls = pageUrls
95
+ get htmlEntrypoints(): Array<HtmlEntrypoint> {
96
+ return Object.values(this.#pages).map(p => p.html)
97
+ }
98
+
99
+ get pageUrls(): Array<string> {
100
+ return Object.keys(this.#pages)
112
101
  }
113
102
 
114
103
  get resolver(): Resolver {
115
104
  return this.#resolver
116
105
  }
117
106
 
118
- // bundleOutputs(type?: 'css' | 'js'): Array<string> {
119
- // if (!type) {
120
- // return Array.from(this.#bundles)
121
- // } else {
122
- // return Array.from(this.#bundles).filter(p => p.endsWith(type))
123
- // }
124
- // }
107
+ get urlRewrites(): Array<UrlRewrite> {
108
+ return Object.values(this.#pages)
109
+ .filter(
110
+ (pr): pr is WebpageRegistration & { urlRewrite: UrlRewrite } =>
111
+ typeof pr.urlRewrite !== 'undefined',
112
+ )
113
+ .map(pr => pr.urlRewrite)
114
+ }
115
+
116
+ get webpageEntryPoints(): Array<EntryPoint> {
117
+ const unique: Set<EntryPoint['in']> = new Set()
118
+ return Object.values(this.#pages)
119
+ .flatMap(p => p.bundles)
120
+ .filter(entryPoint => {
121
+ if (unique.has(entryPoint.in)) {
122
+ return false
123
+ } else {
124
+ unique.add(entryPoint.in)
125
+ return true
126
+ }
127
+ })
128
+ }
129
+
130
+ get webpageAndWorkerEntryPoints(): Array<EntryPoint> {
131
+ const unique: Set<EntryPoint['in']> = new Set()
132
+ const pageBundles = Object.values(this.#pages).flatMap(p => p.bundles)
133
+ const workerBundles = this.workerEntryPoints
134
+ const bundles = workerBundles
135
+ ? [...pageBundles, ...workerBundles]
136
+ : pageBundles
137
+ return bundles.filter(entryPoint => {
138
+ if (unique.has(entryPoint.in)) {
139
+ return false
140
+ } else {
141
+ unique.add(entryPoint.in)
142
+ return true
143
+ }
144
+ })
145
+ }
146
+
147
+ get workerEntryPoints(): Array<EntryPoint> | null {
148
+ return (
149
+ this.#workers?.map(({ workerEntryPoint }) => ({
150
+ in: workerEntryPoint,
151
+ out: workerEntryPoint
152
+ .replace(/^pages[\//]/, '')
153
+ .replace(/\.(mj|t)s$/, '.js'),
154
+ })) || null
155
+ )
156
+ }
157
+
158
+ get workers(): Array<WorkerManifest> | null {
159
+ return this.#workers
160
+ }
125
161
 
126
162
  buildRegistry(): BuildRegistry {
127
- return new BuildRegistry(this.#build, this.#onBuildManifest)
163
+ return new BuildRegistry(
164
+ this.#c.dirs,
165
+ this.#resolver,
166
+ this.#onBuildManifest,
167
+ )
168
+ }
169
+
170
+ configSync() {
171
+ this.#configDiff()
128
172
  }
129
173
 
130
174
  files(): Set<string> {
131
175
  const files = new Set<string>()
132
- for (const pageUrl of this.#pageUrls)
176
+ for (const pageUrl of Object.keys(this.#pages))
133
177
  files.add(pageUrl === '/' ? '/index.html' : `${pageUrl}/index.html`)
134
178
  for (const f of this.#bundles) files.add(f)
135
179
  if (this.#copiedAssets) for (const f of this.#copiedAssets) files.add(f)
@@ -145,27 +189,12 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
145
189
  }
146
190
  }
147
191
 
148
- workerEntryPoints(): Array<EntryPoint> | null {
149
- return (
150
- this.#workers?.map(({ workerEntryPoint }) => ({
151
- in: workerEntryPoint,
152
- out: workerEntryPoint
153
- .replace(/^pages[\//]/, '')
154
- .replace(/\.(mj|t)s$/, '.js'),
155
- })) || null
156
- )
157
- }
158
-
159
- workers(): Array<WorkerManifest> | null {
160
- return this.#workers
161
- }
162
-
163
192
  async writeManifest(buildTag: string): Promise<WebsiteManifest> {
164
193
  const manifest = this.#manifest(buildTag)
165
194
  await writeFile(
166
195
  join(
167
- this.#build.dirs.projectRootAbs,
168
- this.#build.dirs.buildRoot,
196
+ this.#c.dirs.projectRootAbs,
197
+ this.#c.dirs.buildRoot,
169
198
  'website.json',
170
199
  ),
171
200
  JSON.stringify(
@@ -181,11 +210,103 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
181
210
  return manifest
182
211
  }
183
212
 
213
+ #configDiff() {
214
+ const updatePages: ResolvedDankConfig['pages'] = this.#c.devPages
215
+ ? { ...this.#c.pages, ...this.#c.devPages }
216
+ : { ...this.#c.pages }
217
+ const prevPages = new Set(Object.keys(this.#pages))
218
+ for (const [urlPath, mapping] of Object.entries(updatePages)) {
219
+ const existingPage = prevPages.delete(urlPath as `/${string}`)
220
+ if (existingPage) {
221
+ this.#configPageUpdate(urlPath as `/${string}`, mapping)
222
+ } else {
223
+ this.#configPageAdd(urlPath as `/${string}`, mapping)
224
+ }
225
+ }
226
+ for (const prevPage of prevPages) {
227
+ this.#configPageRemove(prevPage as `/${string}`)
228
+ }
229
+ }
230
+
231
+ #configPageAdd(urlPath: `/${string}`, mapping: PageMapping) {
232
+ LOG({
233
+ realm: 'registry',
234
+ message: 'added page',
235
+ data: {
236
+ urlPath,
237
+ srcPath: mapping.webpage,
238
+ },
239
+ })
240
+ const html = new HtmlEntrypoint(
241
+ this.#c,
242
+ this.#resolver,
243
+ urlPath as `/${string}`,
244
+ mapping.webpage,
245
+ )
246
+ const urlRewrite = mapping.pattern
247
+ ? { pattern: mapping.pattern, url: urlPath }
248
+ : undefined
249
+ this.#pages[urlPath as `/${string}`] = {
250
+ pageUrl: urlPath as `/${string}`,
251
+ fsPath: mapping.webpage,
252
+ html,
253
+ urlRewrite,
254
+ bundles: [],
255
+ }
256
+ html.on('entrypoints', entrypoints =>
257
+ this.#setWebpageBundles(html.url, entrypoints),
258
+ )
259
+ this.emit('webpage', html)
260
+ }
261
+
262
+ #configPageUpdate(urlPath: `/${string}`, mapping: PageMapping) {
263
+ const existingRegistration = this.#pages[urlPath as `/${string}`]
264
+ if (existingRegistration.fsPath !== mapping.webpage) {
265
+ this.#configPageRemove(urlPath)
266
+ this.#configPageAdd(urlPath, mapping)
267
+ } else if (
268
+ existingRegistration.urlRewrite?.pattern.source !==
269
+ mapping.pattern?.source
270
+ ) {
271
+ if (mapping.pattern) {
272
+ existingRegistration.urlRewrite = {
273
+ url: urlPath,
274
+ pattern: mapping.pattern,
275
+ }
276
+ } else {
277
+ existingRegistration.urlRewrite = undefined
278
+ }
279
+ }
280
+ LOG({
281
+ realm: 'registry',
282
+ message: 'updated page src',
283
+ data: {
284
+ urlPath,
285
+ newSrcPath: mapping.webpage,
286
+ oldSrcPath: this.#pages[urlPath as `/${string}`].fsPath,
287
+ },
288
+ })
289
+ }
290
+
291
+ #configPageRemove(urlPath: `/${string}`) {
292
+ const registration = this.#pages[urlPath]
293
+ LOG({
294
+ realm: 'registry',
295
+ message: 'removed page',
296
+ data: {
297
+ urlPath,
298
+ srcPath: registration.fsPath,
299
+ },
300
+ })
301
+ registration.html.removeAllListeners()
302
+ delete this.#pages[urlPath]
303
+ }
304
+
184
305
  #manifest(buildTag: string): WebsiteManifest {
185
306
  return {
186
307
  buildTag,
187
308
  files: this.files(),
188
- pageUrls: new Set(this.#pageUrls),
309
+ pageUrls: new Set(Object.keys(this.#pages)),
189
310
  }
190
311
  }
191
312
 
@@ -246,20 +367,32 @@ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
246
367
  this.emit('workers')
247
368
  }
248
369
  }
370
+
371
+ #setWebpageBundles(url: `/${string}`, bundles: Array<EntryPoint>) {
372
+ this.#pages[url].bundles = bundles
373
+ this.emit('entrypoints')
374
+ }
249
375
  }
250
376
 
251
377
  // result accumulator of an esbuild `build` or `Context.rebuild`
252
378
  export class BuildRegistry {
379
+ #dirs: DankDirectories
253
380
  #onComplete: OnBuildComplete
254
381
  #resolver: Resolver
255
382
  #workers: Array<Omit<WorkerManifest, 'dependentEntryPoint'>> | null = null
256
383
 
257
384
  constructor(
258
- build: DankBuild,
385
+ dirs: DankDirectories,
386
+ resolver: Resolver,
259
387
  onComplete: (manifest: BuildManifest) => void,
260
388
  ) {
389
+ this.#dirs = dirs
261
390
  this.#onComplete = onComplete
262
- this.#resolver = new ResolverImpl(build.dirs)
391
+ this.#resolver = resolver
392
+ }
393
+
394
+ get dirs(): DankDirectories {
395
+ return this.#dirs
263
396
  }
264
397
 
265
398
  get resolver(): Resolver {