@eighty4/dank 0.0.3 → 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/esbuild.ts CHANGED
@@ -1,72 +1,116 @@
1
+ import { readFile } from 'node:fs/promises'
1
2
  import esbuild, {
2
3
  type BuildContext,
3
4
  type BuildOptions,
4
5
  type BuildResult,
5
- type Message,
6
- type Metafile,
6
+ type Location,
7
+ type OnLoadArgs,
8
+ type PartialMessage,
9
+ type Plugin,
10
+ type PluginBuild,
7
11
  } from 'esbuild'
12
+ import type { EsbuildConfig } from './dank.ts'
8
13
  import type { DefineDankGlobal } from './define.ts'
9
- import { willMinify } from './flags.ts'
14
+ import type { DankBuild } from './flags.ts'
15
+ import type { BuildRegistry, WebsiteRegistry } from './metadata.ts'
10
16
 
11
- const jsBuildOptions: BuildOptions & { metafile: true; write: true } = {
12
- bundle: true,
13
- metafile: true,
14
- minify: willMinify(),
15
- platform: 'browser',
16
- splitting: false,
17
- treeShaking: true,
18
- write: true,
19
- }
20
-
21
- const webpageBuildOptions: BuildOptions & { metafile: true; write: true } = {
22
- assetNames: 'assets/[name]-[hash]',
23
- format: 'esm',
24
- loader: {
25
- '.tff': 'file',
26
- '.woff': 'file',
27
- '.woff2': 'file',
28
- },
29
- ...jsBuildOptions,
30
- }
17
+ export type EntryPoint = { in: string; out: string }
31
18
 
32
19
  export async function esbuildDevContext(
20
+ b: DankBuild,
21
+ r: WebsiteRegistry,
33
22
  define: DefineDankGlobal,
34
- entryPoints: Array<{ in: string; out: string }>,
35
- outdir: string,
23
+ entryPoints: Array<EntryPoint>,
24
+ c?: EsbuildConfig,
36
25
  ): Promise<BuildContext> {
37
26
  return await esbuild.context({
38
27
  define,
39
28
  entryNames: '[dir]/[name]',
40
- entryPoints: removeEntryPointOutExt(entryPoints),
41
- outdir,
42
- ...webpageBuildOptions,
43
- metafile: false,
29
+ entryPoints: mapEntryPointPaths(entryPoints),
30
+ outdir: b.dirs.buildWatch,
31
+ ...commonBuildOptions(b, r, c),
32
+ splitting: false,
44
33
  write: false,
45
34
  })
46
35
  }
47
36
 
48
37
  export async function esbuildWebpages(
38
+ b: DankBuild,
39
+ r: WebsiteRegistry,
49
40
  define: DefineDankGlobal,
50
- entryPoints: Array<{ in: string; out: string }>,
51
- outdir: string,
52
- ): Promise<Metafile> {
53
- const buildResult = await esbuild.build({
54
- define,
55
- entryNames: '[dir]/[name]-[hash]',
56
- entryPoints: removeEntryPointOutExt(entryPoints),
57
- outdir,
58
- ...webpageBuildOptions,
59
- })
60
- esbuildResultChecks(buildResult)
61
- return buildResult.metafile
41
+ entryPoints: Array<EntryPoint>,
42
+ c?: EsbuildConfig,
43
+ ): Promise<void> {
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
+ }
55
+ }
56
+
57
+ export async function esbuildWorkers(
58
+ b: DankBuild,
59
+ r: WebsiteRegistry,
60
+ define: DefineDankGlobal,
61
+ entryPoints: Array<EntryPoint>,
62
+ c?: EsbuildConfig,
63
+ ): Promise<void> {
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
+ }
79
+ }
80
+
81
+ function commonBuildOptions(
82
+ b: DankBuild,
83
+ r: WebsiteRegistry,
84
+ c?: EsbuildConfig,
85
+ ): BuildOptions {
86
+ const p = workersPlugin(r.buildRegistry())
87
+ return {
88
+ absWorkingDir: b.dirs.projectRootAbs,
89
+ assetNames: 'assets/[name]-[hash]',
90
+ bundle: true,
91
+ format: 'esm',
92
+ loader: c?.loaders || defaultLoaders(),
93
+ metafile: true,
94
+ minify: b.minify,
95
+ platform: 'browser',
96
+ plugins: c?.plugins?.length ? [p, ...c.plugins] : [p],
97
+ splitting: true,
98
+ treeShaking: true,
99
+ write: true,
100
+ }
101
+ }
102
+
103
+ function defaultLoaders(): BuildOptions['loader'] {
104
+ return {
105
+ '.woff': 'file',
106
+ '.woff2': 'file',
107
+ }
62
108
  }
63
109
 
64
110
  // esbuild will append the .js or .css to output filenames
65
111
  // keeping extension on entryPoints data for consistency
66
- // and removing and mapping entryPoints to pass to esbuild
67
- function removeEntryPointOutExt(
68
- entryPoints: Array<{ in: string; out: string }>,
69
- ) {
112
+ // and only trimming when creating esbuild opts
113
+ function mapEntryPointPaths(entryPoints: Array<EntryPoint>) {
70
114
  return entryPoints.map(entryPoint => {
71
115
  return {
72
116
  in: entryPoint.in,
@@ -75,23 +119,158 @@ function removeEntryPointOutExt(
75
119
  })
76
120
  }
77
121
 
78
- function esbuildResultChecks(buildResult: BuildResult) {
79
- if (buildResult.errors.length) {
80
- buildResult.errors.forEach(msg => esbuildPrintMessage(msg, 'warning'))
81
- process.exit(1)
122
+ const WORKER_CTOR_REGEX =
123
+ /new(?:\s|\r?\n)+(?<ctor>(?:Shared)?Worker)(?:\s|\r?\n)*\((?:\s|\r?\n)*(?<url>.*?)(?:\s|\r?\n)*(?<end>[\),])/g
124
+ const WORKER_URL_REGEX = /^('.*'|".*")$/
125
+
126
+ export function workersPlugin(r: BuildRegistry): Plugin {
127
+ return {
128
+ name: '@eighty4/dank/esbuild/workers',
129
+ setup(build: PluginBuild) {
130
+ if (!build.initialOptions.absWorkingDir)
131
+ throw TypeError('plugin requires absWorkingDir')
132
+ if (!build.initialOptions.metafile)
133
+ throw TypeError('plugin requires metafile')
134
+ const { absWorkingDir } = build.initialOptions
135
+
136
+ build.onLoad({ filter: /\.(t|m?j)s$/ }, async args => {
137
+ let contents = await readFile(args.path, 'utf8')
138
+ let offset = 0
139
+ let errors: Array<PartialMessage> | undefined = undefined
140
+ for (const workerCtorMatch of contents.matchAll(
141
+ WORKER_CTOR_REGEX,
142
+ )) {
143
+ if (!WORKER_URL_REGEX.test(workerCtorMatch.groups!.url)) {
144
+ if (!errors) errors = []
145
+ errors.push(
146
+ invalidWorkerUrlCtorArg(
147
+ locationFromMatch(
148
+ args,
149
+ contents,
150
+ workerCtorMatch,
151
+ ),
152
+ workerCtorMatch,
153
+ ),
154
+ )
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
+ ),
182
+ )
183
+ continue
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
+ })
205
+ }
206
+ const loader = args.path.endsWith('ts') ? 'ts' : 'js'
207
+ return { contents, errors, loader }
208
+ })
209
+
210
+ build.onEnd((result: BuildResult<{ metafile: true }>) => {
211
+ if (result.metafile) {
212
+ r.completeBuild(result)
213
+ }
214
+ })
215
+ },
216
+ }
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
82
225
  }
83
- if (buildResult.warnings.length) {
84
- buildResult.warnings.forEach(msg => esbuildPrintMessage(msg, 'warning'))
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,
85
253
  }
86
254
  }
87
255
 
88
- function esbuildPrintMessage(msg: Message, category: 'error' | 'warning') {
89
- const location = msg.location
90
- ? ` (${msg.location.file}L${msg.location.line}:${msg.location.column})`
91
- : ''
92
- console.error(`esbuild ${category}${location}:`, msg.text)
93
- msg.notes.forEach(note => {
94
- console.error(' ', note.text)
95
- if (note.location) console.error(' ', note.location)
96
- })
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
+ }
97
276
  }
package/lib/flags.ts CHANGED
@@ -1,22 +1,166 @@
1
- // `dank serve` will print http access logs to console
2
- export const isLogHttp = () =>
3
- process.env.LOG_HTTP === 'true' || process.argv.includes('--log-http')
1
+ import { join, resolve } from 'node:path'
2
+ import { cwd } from 'node:process'
3
+ import type { DankConfig } from './dank.ts'
4
+
5
+ export type DankBuild = {
6
+ dirs: ProjectDirs
7
+ minify: boolean
8
+ production: boolean
9
+ }
10
+
11
+ export type ProjectDirs = {
12
+ buildRoot: string
13
+ buildWatch: string
14
+ buildDist: string
15
+ pages: string
16
+ pagesResolved: string
17
+ projectResolved: string
18
+ projectRootAbs: string
19
+ public: string
20
+ }
21
+
22
+ export function resolveBuildFlags(): DankBuild {
23
+ const flags: DankBuild = {
24
+ dirs: defaultProjectDirs(cwd()),
25
+ minify: willMinify(),
26
+ production: isProductionBuild(),
27
+ }
28
+ return {
29
+ get dirs(): ProjectDirs {
30
+ return flags.dirs
31
+ },
32
+ get minify(): boolean {
33
+ return flags.minify
34
+ },
35
+ get production(): boolean {
36
+ return flags.production
37
+ },
38
+ }
39
+ }
40
+
41
+ export type DankServe = DankBuild & {
42
+ dankPort: number
43
+ esbuildPort: number
44
+ logHttp: boolean
45
+ preview: boolean
46
+ }
47
+
48
+ export function resolveServeFlags(c: DankConfig): DankServe {
49
+ const preview = isPreviewBuild()
50
+ const flags: DankServe = {
51
+ dirs: defaultProjectDirs(cwd()),
52
+ dankPort: dankPort(c, preview),
53
+ esbuildPort: esbuildPort(c),
54
+ logHttp: willLogHttp(),
55
+ minify: willMinify(),
56
+ preview,
57
+ production: isProductionBuild(),
58
+ }
59
+ return {
60
+ get dirs(): ProjectDirs {
61
+ return flags.dirs
62
+ },
63
+ get dankPort(): number {
64
+ return flags.dankPort
65
+ },
66
+ get esbuildPort(): number {
67
+ return flags.esbuildPort
68
+ },
69
+ get logHttp(): boolean {
70
+ return flags.logHttp
71
+ },
72
+ get minify(): boolean {
73
+ return flags.minify
74
+ },
75
+ get preview(): boolean {
76
+ return flags.preview
77
+ },
78
+ get production(): boolean {
79
+ return flags.production
80
+ },
81
+ }
82
+ }
4
83
 
5
84
  // `dank serve` will pre-bundle and use service worker
6
- export const isPreviewBuild = () =>
85
+ const isPreviewBuild = () =>
7
86
  process.env.PREVIEW === 'true' || process.argv.includes('--preview')
8
87
 
9
88
  // `dank build` will minify sources and append git release tag to build tag
10
89
  // `dank serve` will pre-bundle with service worker and minify
11
- export const isProductionBuild = () =>
90
+ const isProductionBuild = () =>
12
91
  process.env.PRODUCTION === 'true' || process.argv.includes('--production')
13
92
 
14
- export const willMinify = () =>
93
+ // `dank serve` dank port for frontend webserver
94
+ // alternate --preview port for service worker builds
95
+ function dankPort(c: DankConfig, preview: boolean): number {
96
+ if (process.env.DANK_PORT?.length) {
97
+ return parsePortEnvVar('DANK_PORT')
98
+ }
99
+ return preview ? c.previewPort || c.port || 4000 : c.port || 3000
100
+ }
101
+
102
+ // `dank serve` esbuild port for bundler integration
103
+ function esbuildPort(c: DankConfig): number {
104
+ if (process.env.ESBUILD_PORT?.length) {
105
+ return parsePortEnvVar('ESBUILD_PORT')
106
+ }
107
+ return c.esbuild?.port || 3995
108
+ }
109
+
110
+ function parsePortEnvVar(name: string): number {
111
+ const port = parseInt(process.env[name]!, 10)
112
+ if (isNaN(port)) {
113
+ throw Error(`env var ${name}=${port} must be a valid port number`)
114
+ } else {
115
+ return port
116
+ }
117
+ }
118
+
119
+ export function defaultProjectDirs(projectRootAbs: string): ProjectDirs {
120
+ const pages = 'pages'
121
+ const dirs: ProjectDirs = {
122
+ buildRoot: 'build',
123
+ buildDist: join('build', 'dist'),
124
+ buildWatch: join('build', 'watch'),
125
+ pages,
126
+ pagesResolved: resolve(join(projectRootAbs, pages)),
127
+ projectResolved: resolve(projectRootAbs),
128
+ projectRootAbs,
129
+ public: 'public',
130
+ }
131
+ return {
132
+ get buildRoot(): string {
133
+ return dirs.buildRoot
134
+ },
135
+ get buildDist(): string {
136
+ return dirs.buildDist
137
+ },
138
+ get buildWatch(): string {
139
+ return dirs.buildWatch
140
+ },
141
+ get pages(): string {
142
+ return dirs.pages
143
+ },
144
+ get pagesResolved(): string {
145
+ return dirs.pagesResolved
146
+ },
147
+ get projectResolved(): string {
148
+ return dirs.projectResolved
149
+ },
150
+ get projectRootAbs(): string {
151
+ return dirs.projectRootAbs
152
+ },
153
+ get public(): string {
154
+ return dirs.public
155
+ },
156
+ }
157
+ }
158
+
159
+ const willMinify = () =>
15
160
  isProductionBuild() ||
16
161
  process.env.MINIFY === 'true' ||
17
162
  process.argv.includes('--minify')
18
163
 
19
- export const willTsc = () =>
20
- isProductionBuild() ||
21
- process.env.TSC === 'true' ||
22
- process.argv.includes('--tsc')
164
+ // `dank serve` will print http access logs to console
165
+ const willLogHttp = () =>
166
+ process.env.LOG_HTTP === 'true' || process.argv.includes('--log-http')