@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 +6 -1
- package/lib/esbuild.ts +146 -118
- package/lib/flags.ts +9 -3
- package/lib/html.ts +10 -14
- package/lib/metadata.ts +65 -40
- package/lib/serve.ts +1 -0
- package/lib_js/build.js +1 -1
- package/lib_js/esbuild.js +104 -97
- package/lib_js/flags.js +7 -2
- package/lib_js/html.js +8 -13
- package/lib_js/metadata.js +39 -27
- package/lib_js/serve.js +1 -1
- package/package.json +1 -1
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(
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
145
|
+
errors.push(
|
|
146
|
+
invalidWorkerUrlCtorArg(
|
|
147
|
+
locationFromMatch(
|
|
148
|
+
args,
|
|
149
|
+
contents,
|
|
150
|
+
workerCtorMatch,
|
|
151
|
+
),
|
|
152
|
+
workerCtorMatch,
|
|
153
|
+
),
|
|
216
154
|
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
pagesResolved: resolve(join(projectRootAbs,
|
|
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
|
|
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(
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
91
|
-
pagesResolved: resolve(join(projectRootAbs,
|
|
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
|
|
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(
|
|
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 (!
|
|
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);
|
package/lib_js/metadata.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, resolve
|
|
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] ||
|