@eighty4/dank 0.0.4-0 → 0.0.4-1

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
@@ -51,7 +51,12 @@ async function buildWebpages(
51
51
  const htmlEntrypoints: Array<HtmlEntrypoint> = []
52
52
  for (const [urlPath, mapping] of Object.entries(c.pages)) {
53
53
  const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage
54
- const html = new HtmlEntrypoint(build, urlPath, fsPath)
54
+ const html = new HtmlEntrypoint(
55
+ build,
56
+ registry.resolver,
57
+ urlPath,
58
+ fsPath,
59
+ )
55
60
  loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)))
56
61
  htmlEntrypoints.push(html)
57
62
  }
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
  }
@@ -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
  }
package/lib/serve.ts CHANGED
@@ -153,6 +153,7 @@ async function startDevMode(
153
153
  await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true })
154
154
  const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(
155
155
  serve,
156
+ registry.resolver,
156
157
  urlPath,
157
158
  srcPath,
158
159
  [{ type: 'script', js: clientJS }],
package/lib_js/build.js CHANGED
@@ -35,7 +35,7 @@ async function buildWebpages(c, registry, build, define) {
35
35
  const htmlEntrypoints = [];
36
36
  for (const [urlPath, mapping] of Object.entries(c.pages)) {
37
37
  const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage;
38
- const html = new HtmlEntrypoint(build, urlPath, fsPath);
38
+ const html = new HtmlEntrypoint(build, registry.resolver, urlPath, fsPath);
39
39
  loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)));
40
40
  htmlEntrypoints.push(html);
41
41
  }
package/lib_js/esbuild.js CHANGED
@@ -12,28 +12,36 @@ export async function esbuildDevContext(b, r, define, entryPoints, c) {
12
12
  });
13
13
  }
14
14
  export async function esbuildWebpages(b, r, define, entryPoints, c) {
15
- const result = await esbuild.build({
16
- define,
17
- entryNames: '[dir]/[name]-[hash]',
18
- entryPoints: mapEntryPointPaths(entryPoints),
19
- outdir: b.dirs.buildDist,
20
- ...commonBuildOptions(b, r, c),
21
- });
22
- esbuildResultChecks(result);
15
+ try {
16
+ await esbuild.build({
17
+ define,
18
+ entryNames: '[dir]/[name]-[hash]',
19
+ entryPoints: mapEntryPointPaths(entryPoints),
20
+ outdir: b.dirs.buildDist,
21
+ ...commonBuildOptions(b, r, c),
22
+ });
23
+ }
24
+ catch (ignore) {
25
+ process.exit(1);
26
+ }
23
27
  }
24
28
  export async function esbuildWorkers(b, r, define, entryPoints, c) {
25
- const result = await esbuild.build({
26
- define,
27
- entryNames: '[dir]/[name]-[hash]',
28
- entryPoints: mapEntryPointPaths(entryPoints),
29
- outdir: b.dirs.buildDist,
30
- ...commonBuildOptions(b, r, c),
31
- splitting: false,
32
- metafile: true,
33
- write: true,
34
- assetNames: 'assets/[name]-[hash]',
35
- });
36
- esbuildResultChecks(result);
29
+ try {
30
+ await esbuild.build({
31
+ define,
32
+ entryNames: '[dir]/[name]-[hash]',
33
+ entryPoints: mapEntryPointPaths(entryPoints),
34
+ outdir: b.dirs.buildDist,
35
+ ...commonBuildOptions(b, r, c),
36
+ splitting: false,
37
+ metafile: true,
38
+ write: true,
39
+ assetNames: 'assets/[name]-[hash]',
40
+ });
41
+ }
42
+ catch (ignore) {
43
+ process.exit(1);
44
+ }
37
45
  }
38
46
  function commonBuildOptions(b, r, c) {
39
47
  const p = workersPlugin(r.buildRegistry());
@@ -69,27 +77,7 @@ function mapEntryPointPaths(entryPoints) {
69
77
  };
70
78
  });
71
79
  }
72
- function esbuildResultChecks(buildResult) {
73
- if (buildResult.errors.length) {
74
- buildResult.errors.forEach(msg => esbuildPrintMessage(msg, 'warning'));
75
- process.exit(1);
76
- }
77
- if (buildResult.warnings.length) {
78
- buildResult.warnings.forEach(msg => esbuildPrintMessage(msg, 'warning'));
79
- }
80
- }
81
- function esbuildPrintMessage(msg, category) {
82
- const location = msg.location
83
- ? ` (${msg.location.file}L${msg.location.line}:${msg.location.column})`
84
- : '';
85
- console.error(`esbuild ${category}${location}:`, msg.text);
86
- msg.notes.forEach(note => {
87
- console.error(' ', note.text);
88
- if (note.location)
89
- console.error(' ', note.location);
90
- });
91
- }
92
- const WORKER_CTOR_REGEX = /new(?:\s|\r?\n)+Worker(?:\s|\r?\n)*\((?:\s|\r?\n)*(?<url>.*)(?:\s|\r?\n)*\)/g;
80
+ const WORKER_CTOR_REGEX = /new(?:\s|\r?\n)+(?<ctor>(?:Shared)?Worker)(?:\s|\r?\n)*\((?:\s|\r?\n)*(?<url>.*?)(?:\s|\r?\n)*(?<end>[\),])/g;
93
81
  const WORKER_URL_REGEX = /^('.*'|".*")$/;
94
82
  export function workersPlugin(r) {
95
83
  return {
@@ -105,66 +93,44 @@ export function workersPlugin(r) {
105
93
  let offset = 0;
106
94
  let errors = undefined;
107
95
  for (const workerCtorMatch of contents.matchAll(WORKER_CTOR_REGEX)) {
108
- const workerUrlString = workerCtorMatch.groups.url;
109
- if (WORKER_URL_REGEX.test(workerUrlString)) {
110
- const preamble = contents.substring(0, workerCtorMatch.index);
111
- const lineIndex = preamble.lastIndexOf('\n') || 0;
112
- const lineCommented = /\/\//.test(preamble.substring(lineIndex));
113
- if (lineCommented)
114
- continue;
115
- const blockCommentIndex = preamble.lastIndexOf('/*');
116
- const blockCommented = blockCommentIndex !== -1 &&
117
- preamble
118
- .substring(blockCommentIndex)
119
- .indexOf('*/') === -1;
120
- if (blockCommented)
121
- continue;
122
- const clientScript = args.path
123
- .replace(absWorkingDir, '')
124
- .substring(1);
125
- const workerUrl = workerUrlString.substring(1, workerUrlString.length - 1);
126
- // todo out of bounds error on path resolve
127
- const workerEntryPoint = r.resolve(clientScript, workerUrl);
128
- const workerUrlPlaceholder = workerEntryPoint
129
- .replace(/^pages/, '')
130
- .replace(/\.(t|m?j)s$/, '.js');
131
- const workerCtorReplacement = `new Worker('${workerUrlPlaceholder}')`;
132
- contents =
133
- contents.substring(0, workerCtorMatch.index + offset) +
134
- workerCtorReplacement +
135
- contents.substring(workerCtorMatch.index +
136
- workerCtorMatch[0].length +
137
- offset);
138
- offset +=
139
- workerCtorReplacement.length -
140
- workerCtorMatch[0].length;
141
- r.addWorker({
142
- clientScript,
143
- workerEntryPoint,
144
- workerUrl,
145
- workerUrlPlaceholder,
146
- });
96
+ if (!WORKER_URL_REGEX.test(workerCtorMatch.groups.url)) {
97
+ if (!errors)
98
+ errors = [];
99
+ errors.push(invalidWorkerUrlCtorArg(locationFromMatch(args, contents, workerCtorMatch), workerCtorMatch));
100
+ continue;
101
+ }
102
+ if (isIndexCommented(contents, workerCtorMatch.index)) {
103
+ continue;
147
104
  }
148
- else {
105
+ const clientScript = args.path
106
+ .replace(absWorkingDir, '')
107
+ .substring(1);
108
+ const workerUrl = workerCtorMatch.groups.url.substring(1, workerCtorMatch.groups.url.length - 1);
109
+ const workerEntryPoint = r.resolver.resolveHrefInPagesDir(clientScript, workerUrl);
110
+ if (workerEntryPoint === 'outofbounds') {
149
111
  if (!errors)
150
112
  errors = [];
151
- const preamble = contents.substring(0, workerCtorMatch.index);
152
- const line = preamble.match(/\n/g)?.length || 0;
153
- const lineIndex = preamble.lastIndexOf('\n') || 0;
154
- const column = preamble.length - lineIndex;
155
- const lineText = contents.substring(lineIndex, contents.indexOf('\n', lineIndex) ||
156
- contents.length);
157
- errors.push({
158
- id: 'worker-url-unresolvable',
159
- location: {
160
- lineText,
161
- line,
162
- column,
163
- file: args.path,
164
- length: workerCtorMatch[0].length,
165
- },
166
- });
113
+ errors.push(outofboundsWorkerUrlCtorArg(locationFromMatch(args, contents, workerCtorMatch), workerCtorMatch));
114
+ continue;
167
115
  }
116
+ const workerUrlPlaceholder = workerEntryPoint
117
+ .replace(/^pages/, '')
118
+ .replace(/\.(t|m?j)s$/, '.js');
119
+ const workerCtorReplacement = `new ${workerCtorMatch.groups.ctor}('${workerUrlPlaceholder}'${workerCtorMatch.groups.end}`;
120
+ contents =
121
+ contents.substring(0, workerCtorMatch.index + offset) +
122
+ workerCtorReplacement +
123
+ contents.substring(workerCtorMatch.index +
124
+ workerCtorMatch[0].length +
125
+ offset);
126
+ offset +=
127
+ workerCtorReplacement.length - workerCtorMatch[0].length;
128
+ r.addWorker({
129
+ clientScript,
130
+ workerEntryPoint,
131
+ workerUrl,
132
+ workerUrlPlaceholder,
133
+ });
168
134
  }
169
135
  const loader = args.path.endsWith('ts') ? 'ts' : 'js';
170
136
  return { contents, errors, loader };
@@ -177,3 +143,44 @@ export function workersPlugin(r) {
177
143
  },
178
144
  };
179
145
  }
146
+ function isIndexCommented(contents, index) {
147
+ const preamble = contents.substring(0, index);
148
+ const lineIndex = preamble.lastIndexOf('\n') || 0;
149
+ const lineCommented = /\/\//.test(preamble.substring(lineIndex));
150
+ if (lineCommented) {
151
+ return true;
152
+ }
153
+ const blockCommentIndex = preamble.lastIndexOf('/*');
154
+ const blockCommented = blockCommentIndex !== -1 &&
155
+ preamble.substring(blockCommentIndex).indexOf('*/') === -1;
156
+ return blockCommented;
157
+ }
158
+ function locationFromMatch(args, contents, match) {
159
+ const preamble = contents.substring(0, match.index);
160
+ const line = preamble.match(/\n/g)?.length || 0;
161
+ let lineIndex = preamble.lastIndexOf('\n');
162
+ lineIndex = lineIndex === -1 ? 0 : lineIndex + 1;
163
+ const column = preamble.length - lineIndex;
164
+ const lineText = contents.substring(lineIndex, contents.indexOf('\n', lineIndex) || contents.length);
165
+ return {
166
+ lineText,
167
+ line,
168
+ column,
169
+ file: args.path,
170
+ length: match[0].length,
171
+ };
172
+ }
173
+ function outofboundsWorkerUrlCtorArg(location, workerCtorMatch) {
174
+ return {
175
+ id: 'worker-url-outofbounds',
176
+ text: `The ${workerCtorMatch.groups.ctor} constructor URL arg \`${workerCtorMatch.groups.url}\` cannot resolve to a path outside of the pages directory`,
177
+ location,
178
+ };
179
+ }
180
+ function invalidWorkerUrlCtorArg(location, workerCtorMatch) {
181
+ return {
182
+ id: 'worker-url-unresolvable',
183
+ text: `The ${workerCtorMatch.groups.ctor} constructor URL arg \`${workerCtorMatch.groups.url}\` must be a relative module path`,
184
+ location,
185
+ };
186
+ }
package/lib_js/flags.js CHANGED
@@ -83,12 +83,14 @@ function parsePortEnvVar(name) {
83
83
  }
84
84
  }
85
85
  export function defaultProjectDirs(projectRootAbs) {
86
+ const pages = 'pages';
86
87
  const dirs = {
87
88
  buildRoot: 'build',
88
89
  buildDist: join('build', 'dist'),
89
90
  buildWatch: join('build', 'watch'),
90
- pages: 'pages',
91
- pagesResolved: resolve(join(projectRootAbs, 'pages')),
91
+ pages,
92
+ pagesResolved: resolve(join(projectRootAbs, pages)),
93
+ projectResolved: resolve(projectRootAbs),
92
94
  projectRootAbs,
93
95
  public: 'public',
94
96
  };
@@ -108,6 +110,9 @@ export function defaultProjectDirs(projectRootAbs) {
108
110
  get pagesResolved() {
109
111
  return dirs.pagesResolved;
110
112
  },
113
+ get projectResolved() {
114
+ return dirs.projectResolved;
115
+ },
111
116
  get projectRootAbs() {
112
117
  return dirs.projectRootAbs;
113
118
  },
package/lib_js/html.js 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 { defaultTreeAdapter, parse, parseFragment, serialize, } from 'parse5';
6
6
  export class HtmlEntrypoint extends EventEmitter {
@@ -9,14 +9,17 @@ export class HtmlEntrypoint extends EventEmitter {
9
9
  #document = defaultTreeAdapter.createDocument();
10
10
  // todo cache entrypoints set for quicker diffing
11
11
  // #entrypoints: Set<string> = new Set()
12
+ // path within pages dir omitting pages/ segment
12
13
  #fsPath;
13
14
  #partials = [];
15
+ #resolver;
14
16
  #scripts = [];
15
17
  #update = Object();
16
18
  #url;
17
- constructor(build, url, fsPath, decorations) {
19
+ constructor(build, resolver, url, fsPath, decorations) {
18
20
  super({ captureRejections: true });
19
21
  this.#build = build;
22
+ this.#resolver = resolver;
20
23
  this.#decorations = decorations;
21
24
  this.#url = url;
22
25
  this.#fsPath = fsPath;
@@ -165,7 +168,7 @@ export class HtmlEntrypoint extends EventEmitter {
165
168
  errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be an absolute path`);
166
169
  }
167
170
  const partialPath = join(dirname(this.#fsPath), partialSpecifier);
168
- if (!isPagesSubpathInPagesDir(this.#build, partialPath)) {
171
+ if (!this.#resolver.isPagesSubpathInPagesDir(partialPath)) {
169
172
  errorExit(`partial ${partialSpecifier} in webpage ${this.#fsPath} cannot be outside of the pages directory`);
170
173
  }
171
174
  collection.partials.push({
@@ -194,7 +197,7 @@ export class HtmlEntrypoint extends EventEmitter {
194
197
  }
195
198
  #parseImport(type, href, elem) {
196
199
  const inPath = join(this.#build.dirs.pages, dirname(this.#fsPath), href);
197
- if (!isPathInPagesDir(this.#build, inPath)) {
200
+ if (!this.#resolver.isProjectSubpathInPagesDir(inPath)) {
198
201
  errorExit(`href ${href} in webpage ${this.#fsPath} cannot reference sources outside of the pages directory`);
199
202
  }
200
203
  let outPath = join(dirname(this.#fsPath), href);
@@ -212,15 +215,6 @@ export class HtmlEntrypoint extends EventEmitter {
212
215
  };
213
216
  }
214
217
  }
215
- // check if relative dir is a subpath of pages dir when joined with pages dir
216
- // used if the joined pages dir path is only used for the pages dir check
217
- function isPagesSubpathInPagesDir(build, subpath) {
218
- return isPathInPagesDir(build, join(build.dirs.pages, subpath));
219
- }
220
- // check if subpath joined with pages dir is a subpath of pages dir
221
- function isPathInPagesDir(build, p) {
222
- return resolve(p).startsWith(build.dirs.pagesResolved);
223
- }
224
218
  function getAttr(elem, name) {
225
219
  return elem.attrs.find(attr => attr.name === name);
226
220
  }
@@ -252,6 +246,7 @@ function rewriteHrefs(scripts, hrefs) {
252
246
  }
253
247
  }
254
248
  }
249
+ // todo evented error handling so HtmlEntrypoint can be unit tested
255
250
  function errorExit(msg) {
256
251
  console.log(`\u001b[31merror:\u001b[0m`, msg);
257
252
  process.exit(1);
@@ -1,6 +1,27 @@
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
+ class ResolverImpl {
5
+ #dirs;
6
+ constructor(dirs) {
7
+ this.#dirs = dirs;
8
+ }
9
+ isProjectSubpathInPagesDir(p) {
10
+ return resolve(join(this.#dirs.projectResolved, p)).startsWith(this.#dirs.pagesResolved);
11
+ }
12
+ isPagesSubpathInPagesDir(p) {
13
+ return this.isProjectSubpathInPagesDir(join(this.#dirs.pages, p));
14
+ }
15
+ resolveHrefInPagesDir(from, href) {
16
+ const p = join(dirname(from), href);
17
+ if (this.isProjectSubpathInPagesDir(p)) {
18
+ return p;
19
+ }
20
+ else {
21
+ return 'outofbounds';
22
+ }
23
+ }
24
+ }
4
25
  // manages website resources during `dank build` and `dank serve`
5
26
  export class WebsiteRegistry extends EventEmitter {
6
27
  #build;
@@ -11,10 +32,22 @@ export class WebsiteRegistry extends EventEmitter {
11
32
  // map of entrypoints to their output path
12
33
  #entrypointHrefs = {};
13
34
  #pageUrls = [];
35
+ #resolver;
14
36
  #workers = null;
15
37
  constructor(build) {
16
38
  super();
17
39
  this.#build = build;
40
+ this.#resolver = new ResolverImpl(build.dirs);
41
+ }
42
+ set copiedAssets(copiedAssets) {
43
+ this.#copiedAssets =
44
+ copiedAssets === null ? null : new Set(copiedAssets);
45
+ }
46
+ set pageUrls(pageUrls) {
47
+ this.#pageUrls = pageUrls;
48
+ }
49
+ get resolver() {
50
+ return this.#resolver;
18
51
  }
19
52
  // bundleOutputs(type?: 'css' | 'js'): Array<string> {
20
53
  // if (!type) {
@@ -46,9 +79,6 @@ export class WebsiteRegistry extends EventEmitter {
46
79
  throw Error(`mapped href for ${lookup} not found`);
47
80
  }
48
81
  }
49
- resolve(from, href) {
50
- return resolveImpl(this.#build, from, href);
51
- }
52
82
  workerEntryPoints() {
53
83
  return (this.#workers?.map(({ workerEntryPoint }) => ({
54
84
  in: workerEntryPoint,
@@ -69,13 +99,6 @@ export class WebsiteRegistry extends EventEmitter {
69
99
  }, null, 4));
70
100
  return manifest;
71
101
  }
72
- set copiedAssets(copiedAssets) {
73
- this.#copiedAssets =
74
- copiedAssets === null ? null : new Set(copiedAssets);
75
- }
76
- set pageUrls(pageUrls) {
77
- this.#pageUrls = pageUrls;
78
- }
79
102
  #manifest(buildTag) {
80
103
  return {
81
104
  buildTag,
@@ -136,12 +159,15 @@ export class WebsiteRegistry extends EventEmitter {
136
159
  }
137
160
  // result accumulator of an esbuild `build` or `Context.rebuild`
138
161
  export class BuildRegistry {
139
- #build;
140
162
  #onComplete;
163
+ #resolver;
141
164
  #workers = null;
142
165
  constructor(build, onComplete) {
143
- this.#build = build;
144
166
  this.#onComplete = onComplete;
167
+ this.#resolver = new ResolverImpl(build.dirs);
168
+ }
169
+ get resolver() {
170
+ return this.#resolver;
145
171
  }
146
172
  // resolve web worker imported by a webpage module
147
173
  addWorker(worker) {
@@ -181,18 +207,4 @@ export class BuildRegistry {
181
207
  workers,
182
208
  });
183
209
  }
184
- resolve(from, href) {
185
- return resolveImpl(this.#build, from, href);
186
- }
187
- }
188
- function resolveImpl(build, from, href) {
189
- const { pagesResolved, projectRootAbs } = build.dirs;
190
- const fromDir = dirname(from);
191
- const resolvedFromProjectRoot = join(projectRootAbs, fromDir, href);
192
- if (!resolve(resolvedFromProjectRoot).startsWith(pagesResolved)) {
193
- throw Error(`href ${href} cannot be resolved from pages${sep}${from} to a path outside of the pages directory`);
194
- }
195
- else {
196
- return join(fromDir, href);
197
- }
198
210
  }
package/lib_js/serve.js CHANGED
@@ -101,7 +101,7 @@ async function startDevMode(c, serve, signal) {
101
101
  }));
102
102
  async function addPage(urlPath, srcPath) {
103
103
  await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true });
104
- const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(serve, urlPath, srcPath, [{ type: 'script', js: clientJS }]));
104
+ const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(serve, registry.resolver, urlPath, srcPath, [{ type: 'script', js: clientJS }]));
105
105
  htmlEntrypoint.on('entrypoints', entrypoints => {
106
106
  const pathsIn = new Set(entrypoints.map(e => e.in));
107
107
  if (!entryPointsByUrlPath[urlPath] ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eighty4/dank",
3
- "version": "0.0.4-0",
3
+ "version": "0.0.4-1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Adam McKee Bennett <adam.be.g84d@gmail.com>",