@eighty4/dank 0.0.4-0 → 0.0.4-2

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/esbuild.ts CHANGED
@@ -3,7 +3,8 @@ import esbuild, {
3
3
  type BuildContext,
4
4
  type BuildOptions,
5
5
  type BuildResult,
6
- type Message,
6
+ type Location,
7
+ type OnLoadArgs,
7
8
  type PartialMessage,
8
9
  type Plugin,
9
10
  type PluginBuild,
@@ -40,14 +41,17 @@ export async function esbuildWebpages(
40
41
  entryPoints: Array<EntryPoint>,
41
42
  c?: EsbuildConfig,
42
43
  ): Promise<void> {
43
- const result = await esbuild.build({
44
- define,
45
- entryNames: '[dir]/[name]-[hash]',
46
- entryPoints: mapEntryPointPaths(entryPoints),
47
- outdir: b.dirs.buildDist,
48
- ...commonBuildOptions(b, r, c),
49
- })
50
- esbuildResultChecks(result)
44
+ try {
45
+ await esbuild.build({
46
+ define,
47
+ entryNames: '[dir]/[name]-[hash]',
48
+ entryPoints: mapEntryPointPaths(entryPoints),
49
+ outdir: b.dirs.buildDist,
50
+ ...commonBuildOptions(b, r, c),
51
+ })
52
+ } catch (ignore) {
53
+ process.exit(1)
54
+ }
51
55
  }
52
56
 
53
57
  export async function esbuildWorkers(
@@ -57,18 +61,21 @@ export async function esbuildWorkers(
57
61
  entryPoints: Array<EntryPoint>,
58
62
  c?: EsbuildConfig,
59
63
  ): Promise<void> {
60
- const result = await esbuild.build({
61
- define,
62
- entryNames: '[dir]/[name]-[hash]',
63
- entryPoints: mapEntryPointPaths(entryPoints),
64
- outdir: b.dirs.buildDist,
65
- ...commonBuildOptions(b, r, c),
66
- splitting: false,
67
- metafile: true,
68
- write: true,
69
- assetNames: 'assets/[name]-[hash]',
70
- })
71
- esbuildResultChecks(result)
64
+ try {
65
+ await esbuild.build({
66
+ define,
67
+ entryNames: '[dir]/[name]-[hash]',
68
+ entryPoints: mapEntryPointPaths(entryPoints),
69
+ outdir: b.dirs.buildDist,
70
+ ...commonBuildOptions(b, r, c),
71
+ splitting: false,
72
+ metafile: true,
73
+ write: true,
74
+ assetNames: 'assets/[name]-[hash]',
75
+ })
76
+ } catch (ignore) {
77
+ process.exit(1)
78
+ }
72
79
  }
73
80
 
74
81
  function commonBuildOptions(
@@ -112,29 +119,8 @@ function mapEntryPointPaths(entryPoints: Array<EntryPoint>) {
112
119
  })
113
120
  }
114
121
 
115
- function esbuildResultChecks(buildResult: BuildResult) {
116
- if (buildResult.errors.length) {
117
- buildResult.errors.forEach(msg => esbuildPrintMessage(msg, 'warning'))
118
- process.exit(1)
119
- }
120
- if (buildResult.warnings.length) {
121
- buildResult.warnings.forEach(msg => esbuildPrintMessage(msg, 'warning'))
122
- }
123
- }
124
-
125
- function esbuildPrintMessage(msg: Message, category: 'error' | 'warning') {
126
- const location = msg.location
127
- ? ` (${msg.location.file}L${msg.location.line}:${msg.location.column})`
128
- : ''
129
- console.error(`esbuild ${category}${location}:`, msg.text)
130
- msg.notes.forEach(note => {
131
- console.error(' ', note.text)
132
- if (note.location) console.error(' ', note.location)
133
- })
134
- }
135
-
136
122
  const WORKER_CTOR_REGEX =
137
- /new(?:\s|\r?\n)+Worker(?:\s|\r?\n)*\((?:\s|\r?\n)*(?<url>.*)(?:\s|\r?\n)*\)/g
123
+ /new(?:\s|\r?\n)+(?<ctor>(?:Shared)?Worker)(?:\s|\r?\n)*\((?:\s|\r?\n)*(?<url>.*?)(?:\s|\r?\n)*(?<end>[\),])/g
138
124
  const WORKER_URL_REGEX = /^('.*'|".*")$/
139
125
 
140
126
  export function workersPlugin(r: BuildRegistry): Plugin {
@@ -154,85 +140,68 @@ export function workersPlugin(r: BuildRegistry): Plugin {
154
140
  for (const workerCtorMatch of contents.matchAll(
155
141
  WORKER_CTOR_REGEX,
156
142
  )) {
157
- const workerUrlString = workerCtorMatch.groups!.url
158
- if (WORKER_URL_REGEX.test(workerUrlString)) {
159
- const preamble = contents.substring(
160
- 0,
161
- workerCtorMatch.index,
162
- )
163
- const lineIndex = preamble.lastIndexOf('\n') || 0
164
- const lineCommented = /\/\//.test(
165
- preamble.substring(lineIndex),
166
- )
167
- if (lineCommented) continue
168
- const blockCommentIndex = preamble.lastIndexOf('/*')
169
- const blockCommented =
170
- blockCommentIndex !== -1 &&
171
- preamble
172
- .substring(blockCommentIndex)
173
- .indexOf('*/') === -1
174
- if (blockCommented) continue
175
- const clientScript = args.path
176
- .replace(absWorkingDir, '')
177
- .substring(1)
178
- const workerUrl = workerUrlString.substring(
179
- 1,
180
- workerUrlString.length - 1,
181
- )
182
- // todo out of bounds error on path resolve
183
- const workerEntryPoint = r.resolve(
184
- clientScript,
185
- workerUrl,
186
- )
187
- const workerUrlPlaceholder = workerEntryPoint
188
- .replace(/^pages/, '')
189
- .replace(/\.(t|m?j)s$/, '.js')
190
- const workerCtorReplacement = `new Worker('${workerUrlPlaceholder}')`
191
- contents =
192
- contents.substring(
193
- 0,
194
- workerCtorMatch.index + offset,
195
- ) +
196
- workerCtorReplacement +
197
- contents.substring(
198
- workerCtorMatch.index +
199
- workerCtorMatch[0].length +
200
- offset,
201
- )
202
- offset +=
203
- workerCtorReplacement.length -
204
- workerCtorMatch[0].length
205
- r.addWorker({
206
- clientScript,
207
- workerEntryPoint,
208
- workerUrl,
209
- workerUrlPlaceholder,
210
- })
211
- } else {
143
+ if (!WORKER_URL_REGEX.test(workerCtorMatch.groups!.url)) {
212
144
  if (!errors) errors = []
213
- const preamble = contents.substring(
214
- 0,
215
- workerCtorMatch.index,
145
+ errors.push(
146
+ invalidWorkerUrlCtorArg(
147
+ locationFromMatch(
148
+ args,
149
+ contents,
150
+ workerCtorMatch,
151
+ ),
152
+ workerCtorMatch,
153
+ ),
216
154
  )
217
- const line = preamble.match(/\n/g)?.length || 0
218
- const lineIndex = preamble.lastIndexOf('\n') || 0
219
- const column = preamble.length - lineIndex
220
- const lineText = contents.substring(
221
- lineIndex,
222
- contents.indexOf('\n', lineIndex) ||
223
- contents.length,
155
+ continue
156
+ }
157
+ if (isIndexCommented(contents, workerCtorMatch.index)) {
158
+ continue
159
+ }
160
+ const clientScript = args.path
161
+ .replace(absWorkingDir, '')
162
+ .substring(1)
163
+ const workerUrl = workerCtorMatch.groups!.url.substring(
164
+ 1,
165
+ workerCtorMatch.groups!.url.length - 1,
166
+ )
167
+ const workerEntryPoint = r.resolver.resolveHrefInPagesDir(
168
+ clientScript,
169
+ workerUrl,
170
+ )
171
+ if (workerEntryPoint === 'outofbounds') {
172
+ if (!errors) errors = []
173
+ errors.push(
174
+ outofboundsWorkerUrlCtorArg(
175
+ locationFromMatch(
176
+ args,
177
+ contents,
178
+ workerCtorMatch,
179
+ ),
180
+ workerCtorMatch,
181
+ ),
224
182
  )
225
- errors.push({
226
- id: 'worker-url-unresolvable',
227
- location: {
228
- lineText,
229
- line,
230
- column,
231
- file: args.path,
232
- length: workerCtorMatch[0].length,
233
- },
234
- })
183
+ continue
235
184
  }
185
+ const workerUrlPlaceholder = workerEntryPoint
186
+ .replace(/^pages/, '')
187
+ .replace(/\.(t|m?j)s$/, '.js')
188
+ const workerCtorReplacement = `new ${workerCtorMatch.groups!.ctor}('${workerUrlPlaceholder}'${workerCtorMatch.groups!.end}`
189
+ contents =
190
+ contents.substring(0, workerCtorMatch.index + offset) +
191
+ workerCtorReplacement +
192
+ contents.substring(
193
+ workerCtorMatch.index +
194
+ workerCtorMatch[0].length +
195
+ offset,
196
+ )
197
+ offset +=
198
+ workerCtorReplacement.length - workerCtorMatch[0].length
199
+ r.addWorker({
200
+ clientScript,
201
+ workerEntryPoint,
202
+ workerUrl,
203
+ workerUrlPlaceholder,
204
+ })
236
205
  }
237
206
  const loader = args.path.endsWith('ts') ? 'ts' : 'js'
238
207
  return { contents, errors, loader }
@@ -246,3 +215,62 @@ export function workersPlugin(r: BuildRegistry): Plugin {
246
215
  },
247
216
  }
248
217
  }
218
+
219
+ function isIndexCommented(contents: string, index: number) {
220
+ const preamble = contents.substring(0, index)
221
+ const lineIndex = preamble.lastIndexOf('\n') || 0
222
+ const lineCommented = /\/\//.test(preamble.substring(lineIndex))
223
+ if (lineCommented) {
224
+ return true
225
+ }
226
+ const blockCommentIndex = preamble.lastIndexOf('/*')
227
+ const blockCommented =
228
+ blockCommentIndex !== -1 &&
229
+ preamble.substring(blockCommentIndex).indexOf('*/') === -1
230
+ return blockCommented
231
+ }
232
+
233
+ function locationFromMatch(
234
+ args: OnLoadArgs,
235
+ contents: string,
236
+ match: RegExpExecArray,
237
+ ): Partial<Location> {
238
+ const preamble = contents.substring(0, match.index)
239
+ const line = preamble.match(/\n/g)?.length || 0
240
+ let lineIndex = preamble.lastIndexOf('\n')
241
+ lineIndex = lineIndex === -1 ? 0 : lineIndex + 1
242
+ const column = preamble.length - lineIndex
243
+ const lineText = contents.substring(
244
+ lineIndex,
245
+ contents.indexOf('\n', lineIndex) || contents.length,
246
+ )
247
+ return {
248
+ lineText,
249
+ line,
250
+ column,
251
+ file: args.path,
252
+ length: match[0].length,
253
+ }
254
+ }
255
+
256
+ function outofboundsWorkerUrlCtorArg(
257
+ location: Partial<Location>,
258
+ workerCtorMatch: RegExpExecArray,
259
+ ): PartialMessage {
260
+ return {
261
+ id: 'worker-url-outofbounds',
262
+ text: `The ${workerCtorMatch.groups!.ctor} constructor URL arg \`${workerCtorMatch.groups!.url}\` cannot resolve to a path outside of the pages directory`,
263
+ location,
264
+ }
265
+ }
266
+
267
+ function invalidWorkerUrlCtorArg(
268
+ location: Partial<Location>,
269
+ workerCtorMatch: RegExpExecArray,
270
+ ): PartialMessage {
271
+ return {
272
+ id: 'worker-url-unresolvable',
273
+ text: `The ${workerCtorMatch.groups!.ctor} constructor URL arg \`${workerCtorMatch.groups!.url}\` must be a relative module path`,
274
+ location,
275
+ }
276
+ }
package/lib/flags.ts CHANGED
@@ -8,12 +8,13 @@ export type DankBuild = {
8
8
  production: boolean
9
9
  }
10
10
 
11
- type ProjectDirs = {
11
+ export type ProjectDirs = {
12
12
  buildRoot: string
13
13
  buildWatch: string
14
14
  buildDist: string
15
15
  pages: string
16
16
  pagesResolved: string
17
+ projectResolved: string
17
18
  projectRootAbs: string
18
19
  public: string
19
20
  }
@@ -86,7 +87,7 @@ const isPreviewBuild = () =>
86
87
 
87
88
  // `dank build` will minify sources and append git release tag to build tag
88
89
  // `dank serve` will pre-bundle with service worker and minify
89
- const isProductionBuild = () =>
90
+ export const isProductionBuild = () =>
90
91
  process.env.PRODUCTION === 'true' || process.argv.includes('--production')
91
92
 
92
93
  // `dank serve` dank port for frontend webserver
@@ -116,12 +117,14 @@ function parsePortEnvVar(name: string): number {
116
117
  }
117
118
 
118
119
  export function defaultProjectDirs(projectRootAbs: string): ProjectDirs {
120
+ const pages = 'pages'
119
121
  const dirs: ProjectDirs = {
120
122
  buildRoot: 'build',
121
123
  buildDist: join('build', 'dist'),
122
124
  buildWatch: join('build', 'watch'),
123
- pages: 'pages',
124
- pagesResolved: resolve(join(projectRootAbs, 'pages')),
125
+ pages,
126
+ pagesResolved: resolve(join(projectRootAbs, pages)),
127
+ projectResolved: resolve(projectRootAbs),
125
128
  projectRootAbs,
126
129
  public: 'public',
127
130
  }
@@ -141,6 +144,9 @@ export function defaultProjectDirs(projectRootAbs: string): ProjectDirs {
141
144
  get pagesResolved(): string {
142
145
  return dirs.pagesResolved
143
146
  },
147
+ get projectResolved(): string {
148
+ return dirs.projectResolved
149
+ },
144
150
  get projectRootAbs(): string {
145
151
  return dirs.projectRootAbs
146
152
  },
package/lib/html.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from 'node:events'
2
2
  import { readFile } from 'node:fs/promises'
3
- import { dirname, join, relative, resolve } from 'node:path'
3
+ import { dirname, join, relative } from 'node:path'
4
4
  import { extname } from 'node:path/posix'
5
5
  import {
6
6
  defaultTreeAdapter,
@@ -11,6 +11,7 @@ import {
11
11
  } from 'parse5'
12
12
  import type { EntryPoint } from './esbuild.ts'
13
13
  import type { DankBuild } from './flags.ts'
14
+ import type { Resolver } from './metadata.ts'
14
15
 
15
16
  type CommentNode = DefaultTreeAdapterTypes.CommentNode
16
17
  type Document = DefaultTreeAdapterTypes.Document
@@ -25,6 +26,7 @@ type CollectedImports = {
25
26
 
26
27
  type PartialReference = {
27
28
  commentNode: CommentNode
29
+ // path within pages dir omitting pages/ segment
28
30
  fsPath: string
29
31
  }
30
32
 
@@ -77,20 +79,24 @@ export class HtmlEntrypoint extends EventEmitter<HtmlEntrypointEvents> {
77
79
  #document: Document = defaultTreeAdapter.createDocument()
78
80
  // todo cache entrypoints set for quicker diffing
79
81
  // #entrypoints: Set<string> = new Set()
82
+ // path within pages dir omitting pages/ segment
80
83
  #fsPath: string
81
84
  #partials: Array<PartialContent> = []
85
+ #resolver: Resolver
82
86
  #scripts: Array<ImportedScript> = []
83
87
  #update: Object = Object()
84
88
  #url: string
85
89
 
86
90
  constructor(
87
91
  build: DankBuild,
92
+ resolver: Resolver,
88
93
  url: string,
89
94
  fsPath: string,
90
95
  decorations?: Array<HtmlDecoration>,
91
96
  ) {
92
97
  super({ captureRejections: true })
93
98
  this.#build = build
99
+ this.#resolver = resolver
94
100
  this.#decorations = decorations
95
101
  this.#url = url
96
102
  this.#fsPath = fsPath
@@ -297,7 +303,7 @@ export class HtmlEntrypoint extends EventEmitter<HtmlEntrypointEvents> {
297
303
  dirname(this.#fsPath),
298
304
  partialSpecifier,
299
305
  )
300
- if (!isPagesSubpathInPagesDir(this.#build, partialPath)) {
306
+ if (!this.#resolver.isPagesSubpathInPagesDir(partialPath)) {
301
307
  errorExit(
302
308
  `partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`,
303
309
  )
@@ -338,7 +344,7 @@ export class HtmlEntrypoint extends EventEmitter<HtmlEntrypointEvents> {
338
344
  elem: Element,
339
345
  ): ImportedScript {
340
346
  const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href)
341
- if (!isPathInPagesDir(this.#build, inPath)) {
347
+ if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
342
348
  errorExit(
343
349
  `href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`,
344
350
  )
@@ -362,17 +368,6 @@ export class HtmlEntrypoint extends EventEmitter<HtmlEntrypointEvents> {
362
368
  }
363
369
  }
364
370
 
365
- // check if relative dir is a subpath of pages dir when joined with pages dir
366
- // used if the joined pages dir path is only used for the pages dir check
367
- function isPagesSubpathInPagesDir(build: DankBuild, subpath: string): boolean {
368
- return isPathInPagesDir(build, join(build.dirs.pages, subpath))
369
- }
370
-
371
- // check if subpath joined with pages dir is a subpath of pages dir
372
- function isPathInPagesDir(build: DankBuild, p: string): boolean {
373
- return resolve(p).startsWith(build.dirs.pagesResolved)
374
- }
375
-
376
371
  function getAttr(elem: Element, name: string) {
377
372
  return elem.attrs.find(attr => attr.name === name)
378
373
  }
@@ -411,6 +406,7 @@ function rewriteHrefs(scripts: Array<ImportedScript>, hrefs?: HtmlHrefs) {
411
406
  }
412
407
  }
413
408
 
409
+ // todo evented error handling so HtmlEntrypoint can be unit tested
414
410
  function errorExit(msg: string): never {
415
411
  console.log(`\u001b[31merror:\u001b[0m`, msg)
416
412
  process.exit(1)
package/lib/metadata.ts CHANGED
@@ -1,12 +1,51 @@
1
1
  import EventEmitter from 'node:events'
2
2
  import { writeFile } from 'node:fs/promises'
3
- import { dirname, join, resolve, sep } from 'node:path'
3
+ import { dirname, join, resolve } from 'node:path'
4
4
  import type { BuildResult } from 'esbuild'
5
5
  import type { EntryPoint } from './esbuild.ts'
6
- import type { DankBuild } from './flags.ts'
6
+ import type { DankBuild, ProjectDirs } from './flags.ts'
7
+
8
+ export type ResolveError = 'outofbounds'
7
9
 
8
10
  export type Resolver = {
9
- resolve(from: string, href: string): string | 'outofbounds'
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
+ }
10
49
  }
11
50
 
12
51
  // summary of a website build
@@ -45,10 +84,7 @@ export type WebsiteRegistryEvents = {
45
84
  }
46
85
 
47
86
  // manages website resources during `dank build` and `dank serve`
48
- export class WebsiteRegistry
49
- extends EventEmitter<WebsiteRegistryEvents>
50
- implements Resolver
51
- {
87
+ export class WebsiteRegistry extends EventEmitter<WebsiteRegistryEvents> {
52
88
  #build: DankBuild
53
89
  // paths of bundled esbuild outputs
54
90
  #bundles: Set<string> = new Set()
@@ -57,11 +93,26 @@ export class WebsiteRegistry
57
93
  // map of entrypoints to their output path
58
94
  #entrypointHrefs: Record<string, string | null> = {}
59
95
  #pageUrls: Array<string> = []
96
+ #resolver: Resolver
60
97
  #workers: Array<WorkerManifest> | null = null
61
98
 
62
99
  constructor(build: DankBuild) {
63
100
  super()
64
101
  this.#build = build
102
+ this.#resolver = new ResolverImpl(build.dirs)
103
+ }
104
+
105
+ set copiedAssets(copiedAssets: Array<string> | null) {
106
+ this.#copiedAssets =
107
+ copiedAssets === null ? null : new Set(copiedAssets)
108
+ }
109
+
110
+ set pageUrls(pageUrls: Array<string>) {
111
+ this.#pageUrls = pageUrls
112
+ }
113
+
114
+ get resolver(): Resolver {
115
+ return this.#resolver
65
116
  }
66
117
 
67
118
  // bundleOutputs(type?: 'css' | 'js'): Array<string> {
@@ -94,10 +145,6 @@ export class WebsiteRegistry
94
145
  }
95
146
  }
96
147
 
97
- resolve(from: string, href: string): string {
98
- return resolveImpl(this.#build, from, href)
99
- }
100
-
101
148
  workerEntryPoints(): Array<EntryPoint> | null {
102
149
  return (
103
150
  this.#workers?.map(({ workerEntryPoint }) => ({
@@ -134,15 +181,6 @@ export class WebsiteRegistry
134
181
  return manifest
135
182
  }
136
183
 
137
- set copiedAssets(copiedAssets: Array<string> | null) {
138
- this.#copiedAssets =
139
- copiedAssets === null ? null : new Set(copiedAssets)
140
- }
141
-
142
- set pageUrls(pageUrls: Array<string>) {
143
- this.#pageUrls = pageUrls
144
- }
145
-
146
184
  #manifest(buildTag: string): WebsiteManifest {
147
185
  return {
148
186
  buildTag,
@@ -211,17 +249,21 @@ export class WebsiteRegistry
211
249
  }
212
250
 
213
251
  // result accumulator of an esbuild `build` or `Context.rebuild`
214
- export class BuildRegistry implements Resolver {
215
- #build: DankBuild
252
+ export class BuildRegistry {
216
253
  #onComplete: OnBuildComplete
254
+ #resolver: Resolver
217
255
  #workers: Array<Omit<WorkerManifest, 'dependentEntryPoint'>> | null = null
218
256
 
219
257
  constructor(
220
258
  build: DankBuild,
221
259
  onComplete: (manifest: BuildManifest) => void,
222
260
  ) {
223
- this.#build = build
224
261
  this.#onComplete = onComplete
262
+ this.#resolver = new ResolverImpl(build.dirs)
263
+ }
264
+
265
+ get resolver(): Resolver {
266
+ return this.#resolver
225
267
  }
226
268
 
227
269
  // resolve web worker imported by a webpage module
@@ -264,21 +306,4 @@ export class BuildRegistry implements Resolver {
264
306
  workers,
265
307
  })
266
308
  }
267
-
268
- resolve(from: string, href: string): string {
269
- return resolveImpl(this.#build, from, href)
270
- }
271
- }
272
-
273
- function resolveImpl(build: DankBuild, from: string, href: string): string {
274
- const { pagesResolved, projectRootAbs } = build.dirs
275
- const fromDir = dirname(from)
276
- const resolvedFromProjectRoot = join(projectRootAbs, fromDir, href)
277
- if (!resolve(resolvedFromProjectRoot).startsWith(pagesResolved)) {
278
- throw Error(
279
- `href ${href} cannot be resolved from pages${sep}${from} to a path outside of the pages directory`,
280
- )
281
- } else {
282
- return join(fromDir, href)
283
- }
284
309
  }