@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/serve.ts CHANGED
@@ -1,55 +1,95 @@
1
- import { mkdir, readFile, rm, watch as _watch } from 'node:fs/promises'
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ rm,
5
+ watch as _watch,
6
+ writeFile,
7
+ } from 'node:fs/promises'
2
8
  import { extname, join, resolve } from 'node:path'
3
9
  import type { BuildContext } from 'esbuild'
4
10
  import { buildWebsite } from './build.ts'
5
11
  import { loadConfig } from './config.ts'
6
12
  import type { DankConfig } from './dank.ts'
7
13
  import { createGlobalDefinitions } from './define.ts'
8
- import { esbuildDevContext } from './esbuild.ts'
9
- import { isPreviewBuild } from './flags.ts'
14
+ import { esbuildDevContext, type EntryPoint } from './esbuild.ts'
15
+ import { resolveServeFlags, type DankServe } from './flags.ts'
10
16
  import { HtmlEntrypoint } from './html.ts'
11
17
  import {
12
18
  createBuiltDistFilesFetcher,
13
19
  createDevServeFilesFetcher,
14
- createWebServer,
20
+ startWebServer,
21
+ type PageRouteState,
22
+ type UrlRewrite,
15
23
  } from './http.ts'
24
+ import { WebsiteRegistry } from './metadata.ts'
16
25
  import { startDevServices, updateDevServices } from './services.ts'
17
26
 
18
- const isPreview = isPreviewBuild()
19
-
20
- // alternate port for --preview bc of service worker
21
- const PORT = isPreview ? 4000 : 3000
22
-
23
- // port for esbuild.serve
24
- const ESBUILD_PORT = 2999
25
-
26
27
  export async function serveWebsite(c: DankConfig): Promise<never> {
27
- await rm('build', { force: true, recursive: true })
28
- if (isPreview) {
29
- await startPreviewMode(c)
28
+ const serve = resolveServeFlags(c)
29
+ await rm(serve.dirs.buildRoot, { force: true, recursive: true })
30
+ const abortController = new AbortController()
31
+ process.once('exit', () => abortController.abort())
32
+ if (serve.preview) {
33
+ await startPreviewMode(c, serve, abortController.signal)
30
34
  } else {
31
- const abortController = new AbortController()
32
- await startDevMode(c, abortController.signal)
35
+ await startDevMode(c, serve, abortController.signal)
33
36
  }
34
37
  return new Promise(() => {})
35
38
  }
36
39
 
37
- async function startPreviewMode(c: DankConfig) {
38
- const { dir, files } = await buildWebsite(c)
39
- const frontend = createBuiltDistFilesFetcher(dir, files)
40
- createWebServer(PORT, frontend).listen(PORT)
41
- console.log(`preview is live at http://127.0.0.1:${PORT}`)
40
+ async function startPreviewMode(
41
+ c: DankConfig,
42
+ serve: DankServe,
43
+ signal: AbortSignal,
44
+ ) {
45
+ const manifest = await buildWebsite(c, serve)
46
+ const frontend = createBuiltDistFilesFetcher(serve.dirs.buildDist, manifest)
47
+ const devServices = startDevServices(c, signal)
48
+ startWebServer(serve, frontend, devServices.http, {
49
+ urls: Object.keys(c.pages),
50
+ urlRewrites: collectUrlRewrites(c),
51
+ })
52
+ }
53
+
54
+ function collectUrlRewrites(c: DankConfig): Array<UrlRewrite> {
55
+ return Object.keys(c.pages)
56
+ .sort()
57
+ .map(url => {
58
+ const mapping = c.pages[url as `/${string}`]
59
+ return typeof mapping !== 'object' || !mapping.pattern
60
+ ? null
61
+ : { url, pattern: mapping.pattern }
62
+ })
63
+ .filter(mapping => mapping !== null)
64
+ }
65
+
66
+ type BuildContextState =
67
+ | BuildContext
68
+ | 'starting'
69
+ | 'dirty'
70
+ | 'disposing'
71
+ | null
72
+
73
+ type EntrypointsState = {
74
+ entrypoints: Array<EntryPoint>
75
+ pathsIn: Set<string>
42
76
  }
43
77
 
44
78
  // todo changing partials triggers update on html pages
45
- async function startDevMode(c: DankConfig, signal: AbortSignal) {
46
- const watchDir = join('build', 'watch')
47
- await mkdir(watchDir, { recursive: true })
48
- const clientJS = await loadClientJS()
49
- const pagesByUrlPath: Record<string, WebpageMetadata> = {}
50
- const entryPointsByUrlPath: Record<string, Set<string>> = {}
51
- let buildContext: BuildContext | 'starting' | 'dirty' | 'disposing' | null =
52
- null
79
+ async function startDevMode(
80
+ c: DankConfig,
81
+ serve: DankServe,
82
+ signal: AbortSignal,
83
+ ) {
84
+ await mkdir(serve.dirs.buildWatch, { recursive: true })
85
+ const registry = new WebsiteRegistry(serve)
86
+ const clientJS = await loadClientJS(serve.esbuildPort)
87
+ const pagesByUrlPath: Record<string, HtmlEntrypoint> = {}
88
+ const partialsByUrlPath: Record<string, Array<string>> = {}
89
+ const entryPointsByUrlPath: Record<string, EntrypointsState> = {}
90
+ let buildContext: BuildContextState = null
91
+
92
+ registry.on('workers', resetBuildContext)
53
93
 
54
94
  watch('dank.config.ts', signal, async () => {
55
95
  let updated: DankConfig
@@ -60,15 +100,17 @@ async function startDevMode(c: DankConfig, signal: AbortSignal) {
60
100
  }
61
101
  const prevPages = new Set(Object.keys(pagesByUrlPath))
62
102
  await Promise.all(
63
- Object.entries(updated.pages).map(async ([urlPath, srcPath]) => {
64
- c.pages[urlPath as `/${string}`] = srcPath
65
- if (pagesByUrlPath[urlPath]) {
103
+ Object.entries(updated.pages).map(async ([urlPath, mapping]) => {
104
+ c.pages[urlPath as `/${string}`] = mapping
105
+ const srcPath =
106
+ typeof mapping === 'string' ? mapping : mapping.webpage
107
+ if (!pagesByUrlPath[urlPath]) {
108
+ await addPage(urlPath, srcPath)
109
+ } else {
66
110
  prevPages.delete(urlPath)
67
- if (pagesByUrlPath[urlPath].srcPath !== srcPath) {
111
+ if (pagesByUrlPath[urlPath].fsPath !== srcPath) {
68
112
  await updatePage(urlPath)
69
113
  }
70
- } else {
71
- await addPage(urlPath, srcPath)
72
114
  }
73
115
  }),
74
116
  )
@@ -79,114 +121,165 @@ async function startDevMode(c: DankConfig, signal: AbortSignal) {
79
121
  updateDevServices(updated)
80
122
  })
81
123
 
82
- watch('pages', signal, filename => {
124
+ watch(serve.dirs.pages, signal, filename => {
83
125
  if (extname(filename) === '.html') {
84
126
  for (const [urlPath, srcPath] of Object.entries(c.pages)) {
85
127
  if (srcPath === filename) {
86
128
  updatePage(urlPath)
87
129
  }
88
130
  }
131
+ for (const [urlPath, partials] of Object.entries(
132
+ partialsByUrlPath,
133
+ )) {
134
+ if (partials.includes(filename)) {
135
+ updatePage(urlPath, filename)
136
+ }
137
+ }
89
138
  }
90
139
  })
91
140
 
92
141
  await Promise.all(
93
- Object.entries(c.pages).map(([urlPath, srcPath]) =>
94
- addPage(urlPath, srcPath),
95
- ),
142
+ Object.entries(c.pages).map(async ([urlPath, mapping]) => {
143
+ const srcPath =
144
+ typeof mapping === 'string' ? mapping : mapping.webpage
145
+ await addPage(urlPath, srcPath)
146
+ return new Promise(res =>
147
+ pagesByUrlPath[urlPath].once('entrypoints', res),
148
+ )
149
+ }),
96
150
  )
97
151
 
98
152
  async function addPage(urlPath: string, srcPath: string) {
99
- const metadata = await processWebpage({
100
- clientJS,
101
- outDir: watchDir,
102
- pagesDir: 'pages',
103
- srcPath,
153
+ await mkdir(join(serve.dirs.buildWatch, urlPath), { recursive: true })
154
+ const htmlEntrypoint = (pagesByUrlPath[urlPath] = new HtmlEntrypoint(
155
+ serve,
156
+ registry.resolver,
104
157
  urlPath,
158
+ srcPath,
159
+ [{ type: 'script', js: clientJS }],
160
+ ))
161
+ htmlEntrypoint.on('entrypoints', entrypoints => {
162
+ const pathsIn = new Set(entrypoints.map(e => e.in))
163
+ if (
164
+ !entryPointsByUrlPath[urlPath] ||
165
+ !matchingEntrypoints(
166
+ entryPointsByUrlPath[urlPath].pathsIn,
167
+ pathsIn,
168
+ )
169
+ ) {
170
+ entryPointsByUrlPath[urlPath] = { entrypoints, pathsIn }
171
+ resetBuildContext()
172
+ }
173
+ })
174
+ htmlEntrypoint.on('partial', partial => {
175
+ if (!partialsByUrlPath[urlPath]) {
176
+ partialsByUrlPath[urlPath] = []
177
+ }
178
+ partialsByUrlPath[urlPath].push(partial)
105
179
  })
106
- pagesByUrlPath[urlPath] = metadata
107
- entryPointsByUrlPath[urlPath] = new Set(
108
- metadata.entryPoints.map(e => e.in),
180
+ htmlEntrypoint.on(
181
+ 'partials',
182
+ partials => (partialsByUrlPath[urlPath] = partials),
183
+ )
184
+ htmlEntrypoint.on('output', html =>
185
+ writeFile(join(serve.dirs.buildWatch, urlPath, 'index.html'), html),
109
186
  )
110
- if (buildContext !== null) {
111
- resetBuildContext()
112
- }
113
187
  }
114
188
 
115
189
  function deletePage(urlPath: string) {
190
+ pagesByUrlPath[urlPath].removeAllListeners()
116
191
  delete pagesByUrlPath[urlPath]
117
192
  delete entryPointsByUrlPath[urlPath]
118
193
  resetBuildContext()
119
194
  }
120
195
 
121
- async function updatePage(urlPath: string) {
122
- const update = await processWebpage({
123
- clientJS,
124
- outDir: watchDir,
125
- pagesDir: 'pages',
126
- srcPath: c.pages[urlPath as `/${string}`],
127
- urlPath,
128
- })
129
- const entryPointUrls = new Set(update.entryPoints.map(e => e.in))
130
- if (!hasSameValues(entryPointUrls, entryPointsByUrlPath[urlPath])) {
131
- entryPointsByUrlPath[urlPath] = entryPointUrls
132
- resetBuildContext()
133
- }
196
+ async function updatePage(urlPath: string, partial?: string) {
197
+ pagesByUrlPath[urlPath].emit('change', partial)
134
198
  }
135
199
 
136
- function collectEntrypoints(): Array<{ in: string; out: string }> {
137
- const sources: Set<string> = new Set()
138
- return Object.values(pagesByUrlPath)
139
- .flatMap(({ entryPoints }) => entryPoints)
200
+ function collectEntrypoints(): Array<EntryPoint> {
201
+ const unique: Set<string> = new Set()
202
+ const pageBundles = Object.values(entryPointsByUrlPath)
203
+ .flatMap(entrypointState => entrypointState.entrypoints)
140
204
  .filter(entryPoint => {
141
- if (sources.has(entryPoint.in)) {
205
+ if (unique.has(entryPoint.in)) {
142
206
  return false
143
207
  } else {
144
- sources.add(entryPoint.in)
208
+ unique.add(entryPoint.in)
145
209
  return true
146
210
  }
147
211
  })
212
+ const workerBundles = registry.workerEntryPoints()
213
+ if (workerBundles) {
214
+ return [...pageBundles, ...workerBundles]
215
+ } else {
216
+ return pageBundles
217
+ }
148
218
  }
149
219
 
150
220
  function resetBuildContext() {
151
- if (buildContext === 'starting' || buildContext === 'dirty') {
152
- buildContext = 'dirty'
153
- return
154
- }
155
- if (buildContext === 'disposing') {
156
- return
221
+ switch (buildContext) {
222
+ case 'starting':
223
+ buildContext = 'dirty'
224
+ return
225
+ case 'dirty':
226
+ case 'disposing':
227
+ return
157
228
  }
158
229
  if (buildContext !== null) {
159
- const prev = buildContext
230
+ const disposing = buildContext.dispose()
160
231
  buildContext = 'disposing'
161
- prev.dispose().then(() => {
232
+ disposing.then(() => {
162
233
  buildContext = null
163
234
  resetBuildContext()
164
235
  })
165
236
  } else {
166
- startEsbuildWatch(collectEntrypoints()).then(ctx => {
167
- if (buildContext === 'dirty') {
168
- buildContext = null
169
- resetBuildContext()
170
- } else {
171
- buildContext = ctx
172
- }
173
- })
237
+ buildContext = 'starting'
238
+ startEsbuildWatch(c, registry, serve, collectEntrypoints()).then(
239
+ ctx => {
240
+ if (buildContext === 'dirty') {
241
+ buildContext = 'disposing'
242
+ ctx.dispose().then(() => {
243
+ buildContext = null
244
+ resetBuildContext()
245
+ })
246
+ } else {
247
+ buildContext = ctx
248
+ }
249
+ },
250
+ )
174
251
  }
175
252
  }
176
253
 
177
- buildContext = await startEsbuildWatch(collectEntrypoints())
178
- const frontend = createDevServeFilesFetcher({
179
- pages: c.pages,
180
- pagesDir: watchDir,
181
- proxyPort: ESBUILD_PORT,
182
- publicDir: 'public',
183
- })
184
- createWebServer(PORT, frontend).listen(PORT)
185
- console.log(`dev server is live at http://127.0.0.1:${PORT}`)
186
- startDevServices(c, signal)
254
+ // function removePartialFromPage(partial: string, urlPath: string) {
255
+ // const deleteIndex = urlPathsByPartials[partial].indexOf(urlPath)
256
+ // if (deleteIndex !== -1) {
257
+ // if (urlPathsByPartials[partial].length === 1) {
258
+ // delete urlPathsByPartials[partial]
259
+ // } else {
260
+ // urlPathsByPartials[partial].splice(deleteIndex, 1)
261
+ // }
262
+ // }
263
+ // }
264
+
265
+ // inital start of esbuild ctx
266
+ resetBuildContext()
267
+
268
+ // todo this page route state could be built on change and reused
269
+ const pageRoutes: PageRouteState = {
270
+ get urls(): Array<string> {
271
+ return Object.keys(c.pages)
272
+ },
273
+ get urlRewrites(): Array<UrlRewrite> {
274
+ return collectUrlRewrites(c)
275
+ },
276
+ }
277
+ const frontend = createDevServeFilesFetcher(pageRoutes, serve)
278
+ const devServices = startDevServices(c, signal)
279
+ startWebServer(serve, frontend, devServices.http, pageRoutes)
187
280
  }
188
281
 
189
- function hasSameValues(a: Set<string>, b: Set<string>): boolean {
282
+ function matchingEntrypoints(a: Set<string>, b: Set<string>): boolean {
190
283
  if (a.size !== b.size) {
191
284
  return false
192
285
  }
@@ -198,73 +291,41 @@ function hasSameValues(a: Set<string>, b: Set<string>): boolean {
198
291
  return true
199
292
  }
200
293
 
201
- type WebpageInputs = {
202
- clientJS: string
203
- outDir: string
204
- pagesDir: string
205
- srcPath: string
206
- urlPath: string
207
- }
208
-
209
- type WebpageMetadata = {
210
- entryPoints: Array<{ in: string; out: string }>
211
- srcPath: string
212
- urlPath: string
213
- }
214
-
215
- async function processWebpage(inputs: WebpageInputs): Promise<WebpageMetadata> {
216
- const html = await HtmlEntrypoint.readFrom(
217
- inputs.urlPath,
218
- join(inputs.pagesDir, inputs.srcPath),
219
- )
220
- await html.injectPartials()
221
- if (inputs.urlPath !== '/') {
222
- await mkdir(join(inputs.outDir, inputs.urlPath), { recursive: true })
223
- }
224
- const entryPoints: Array<{ in: string; out: string }> = []
225
- html.collectScripts().forEach(scriptImport => {
226
- entryPoints.push({
227
- in: scriptImport.in,
228
- out: scriptImport.out,
229
- })
230
- })
231
- html.rewriteHrefs()
232
- html.appendScript(inputs.clientJS)
233
- await html.writeTo(inputs.outDir)
234
- return {
235
- entryPoints,
236
- srcPath: inputs.srcPath,
237
- urlPath: inputs.urlPath,
238
- }
239
- }
240
-
241
294
  async function startEsbuildWatch(
242
- entryPoints: Array<{ in: string; out: string }>,
295
+ c: DankConfig,
296
+ registry: WebsiteRegistry,
297
+ serve: DankServe,
298
+ entryPoints: Array<EntryPoint>,
243
299
  ): Promise<BuildContext> {
244
300
  const ctx = await esbuildDevContext(
245
- createGlobalDefinitions(),
301
+ serve,
302
+ registry,
303
+ createGlobalDefinitions(serve),
246
304
  entryPoints,
247
- 'build/watch',
305
+ c.esbuild,
248
306
  )
249
307
 
250
308
  await ctx.watch()
251
309
 
252
310
  await ctx.serve({
253
311
  host: '127.0.0.1',
254
- port: ESBUILD_PORT,
312
+ port: serve.esbuildPort,
255
313
  cors: {
256
- origin: 'http://127.0.0.1:' + PORT,
314
+ origin: ['127.0.0.1', 'localhost'].map(
315
+ hostname => `http://${hostname}:${serve.dankPort}`,
316
+ ),
257
317
  },
258
318
  })
259
319
 
260
320
  return ctx
261
321
  }
262
322
 
263
- async function loadClientJS() {
264
- return await readFile(
323
+ async function loadClientJS(esbuildPort: number) {
324
+ const clientJS = await readFile(
265
325
  resolve(import.meta.dirname, join('..', 'client', 'esbuild.js')),
266
326
  'utf-8',
267
327
  )
328
+ return clientJS.replace('3995', `${esbuildPort}`)
268
329
  }
269
330
 
270
331
  async function watch(
package/lib/services.ts CHANGED
@@ -2,6 +2,16 @@ import { type ChildProcess, spawn } from 'node:child_process'
2
2
  import { basename, isAbsolute, resolve } from 'node:path'
3
3
  import type { DankConfig, DevService } from './dank.ts'
4
4
 
5
+ export type DevServices = {
6
+ http: HttpServices
7
+ }
8
+
9
+ export type HttpServices = {
10
+ running: Array<HttpService>
11
+ }
12
+
13
+ export type HttpService = NonNullable<DevService['http']>
14
+
5
15
  // up to date representation of dank.config.ts services
6
16
  const running: Array<{ s: DevService; process: ChildProcess | null }> = []
7
17
 
@@ -13,13 +23,23 @@ let updating: null | {
13
23
  starting: Array<DevService>
14
24
  } = null
15
25
 
16
- export function startDevServices(c: DankConfig, _signal: AbortSignal) {
26
+ export function startDevServices(
27
+ c: DankConfig,
28
+ _signal: AbortSignal,
29
+ ): DevServices {
17
30
  signal = _signal
18
31
  if (c.services?.length) {
19
32
  for (const s of c.services) {
20
33
  running.push({ s, process: startService(s) })
21
34
  }
22
35
  }
36
+ return {
37
+ http: {
38
+ get running(): Array<NonNullable<DevService['http']>> {
39
+ return running.map(({ s }) => s.http).filter(http => !!http)
40
+ },
41
+ },
42
+ }
23
43
  }
24
44
 
25
45
  export function updateDevServices(c: DankConfig) {
@@ -138,9 +158,13 @@ function startService(s: DevService): ChildProcess {
138
158
  spawned.stderr.on('data', chunk => printChunk(stderrLabel, chunk))
139
159
 
140
160
  spawned.on('error', e => {
141
- const cause =
142
- 'code' in e && e.code === 'ENOENT' ? 'program not found' : e.message
143
- opPrint(s, 'error: ' + cause)
161
+ if (e.name !== 'AbortError') {
162
+ const cause =
163
+ 'code' in e && e.code === 'ENOENT'
164
+ ? 'program not found'
165
+ : e.message
166
+ opPrint(s, 'error: ' + cause)
167
+ }
144
168
  removeFromRunning(s)
145
169
  })
146
170
 
package/lib_js/build.js CHANGED
@@ -1,70 +1,95 @@
1
- import { mkdir, rm } from 'node:fs/promises';
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { isProductionBuild, willMinify } from "./flags.js";
4
- import { copyAssets } from "./public.js";
5
- import { createBuildTag } from "./tag.js";
6
- import { writeBuildManifest, writeMetafile } from "./manifest.js";
3
+ import { createBuildTag } from "./build_tag.js";
7
4
  import { createGlobalDefinitions } from "./define.js";
5
+ import { esbuildWebpages, esbuildWorkers } from "./esbuild.js";
6
+ import { resolveBuildFlags } from "./flags.js";
8
7
  import { HtmlEntrypoint } from "./html.js";
9
- import { esbuildWebpages } from "./esbuild.js";
10
- export async function buildWebsite(c) {
11
- const buildDir = 'build';
12
- const distDir = join(buildDir, 'dist');
13
- const buildTag = await createBuildTag();
14
- console.log(willMinify()
15
- ? isProductionBuild()
8
+ import { WebsiteRegistry } from "./metadata.js";
9
+ import { copyAssets } from "./public.js";
10
+ export async function buildWebsite(c, build = resolveBuildFlags()) {
11
+ const buildTag = await createBuildTag(build);
12
+ console.log(build.minify
13
+ ? build.production
16
14
  ? 'minified production'
17
15
  : 'minified'
18
16
  : 'unminified', 'build', buildTag, 'building in ./build/dist');
19
- await rm(buildDir, { recursive: true, force: true });
20
- await mkdir(distDir, { recursive: true });
21
- await mkdir(join(buildDir, 'metafiles'), { recursive: true });
22
- const staticAssets = await copyAssets(distDir);
23
- const buildUrls = [];
24
- buildUrls.push(...(await buildWebpages(distDir, createGlobalDefinitions(), c.pages)));
25
- if (staticAssets) {
26
- buildUrls.push(...staticAssets);
17
+ await rm(build.dirs.buildRoot, { recursive: true, force: true });
18
+ await mkdir(build.dirs.buildDist, { recursive: true });
19
+ for (const subdir of Object.keys(c.pages).filter(url => url !== '/')) {
20
+ await mkdir(join(build.dirs.buildDist, subdir), { recursive: true });
27
21
  }
28
- const result = new Set(buildUrls);
29
- await writeBuildManifest(buildTag, result);
30
- return {
31
- dir: distDir,
32
- files: result,
33
- };
22
+ await mkdir(join(build.dirs.buildRoot, 'metafiles'), { recursive: true });
23
+ const registry = new WebsiteRegistry(build);
24
+ registry.pageUrls = Object.keys(c.pages);
25
+ registry.copiedAssets = await copyAssets(build);
26
+ await buildWebpages(c, registry, build, createGlobalDefinitions(build));
27
+ return await registry.writeManifest(buildTag);
34
28
  }
35
- async function buildWebpages(distDir, define, pages) {
36
- const entryPointUrls = new Set();
37
- const entryPoints = [];
38
- const htmlEntrypoints = await Promise.all(Object.entries(pages).map(async ([urlPath, fsPath]) => {
39
- const html = await HtmlEntrypoint.readFrom(urlPath, join('pages', fsPath));
40
- await html.injectPartials();
41
- if (urlPath !== '/') {
42
- await mkdir(join(distDir, urlPath), { recursive: true });
29
+ // builds all webpage entrypoints in one esbuild.build context
30
+ // to support code splitting
31
+ // returns all built assets URLs and webpage URLs from DankConfig.pages
32
+ async function buildWebpages(c, registry, build, define) {
33
+ // create HtmlEntrypoint for each webpage and collect awaitable esbuild entrypoints
34
+ const loadingEntryPoints = [];
35
+ const htmlEntrypoints = [];
36
+ for (const [urlPath, mapping] of Object.entries(c.pages)) {
37
+ const fsPath = typeof mapping === 'string' ? mapping : mapping.webpage;
38
+ const html = new HtmlEntrypoint(build, registry.resolver, urlPath, fsPath);
39
+ loadingEntryPoints.push(new Promise(res => html.on('entrypoints', res)));
40
+ htmlEntrypoints.push(html);
41
+ }
42
+ // collect esbuild entrypoints from every HtmlEntrypoint
43
+ const uniqueEntryPoints = new Set();
44
+ const buildEntryPoints = [];
45
+ for (const pageEntryPoints of await Promise.all(loadingEntryPoints)) {
46
+ for (const entryPoint of pageEntryPoints) {
47
+ if (!uniqueEntryPoints.has(entryPoint.in)) {
48
+ buildEntryPoints.push(entryPoint);
49
+ }
43
50
  }
44
- html.collectScripts()
45
- .filter(scriptImport => !entryPointUrls.has(scriptImport.in))
46
- .forEach(scriptImport => {
47
- entryPointUrls.add(scriptImport.in);
48
- entryPoints.push({
49
- in: scriptImport.in,
50
- out: scriptImport.out,
51
- });
52
- });
53
- return html;
54
- }));
55
- const metafile = await esbuildWebpages(define, entryPoints, distDir);
56
- await writeMetafile(`pages.json`, metafile);
57
- // todo these hrefs would have \ path separators on windows
58
- const buildUrls = [...Object.keys(pages)];
59
- const mapInToOutHrefs = {};
60
- for (const [outputFile, { entryPoint }] of Object.entries(metafile.outputs)) {
61
- const outputUrl = outputFile.replace(/^build\/dist/, '');
62
- buildUrls.push(outputUrl);
63
- mapInToOutHrefs[entryPoint] = outputUrl;
64
51
  }
52
+ await esbuildWebpages(build, registry, define, buildEntryPoints, c.esbuild);
53
+ // todo recursively build workers on building workers that create workers
54
+ const workerEntryPoints = registry.workerEntryPoints();
55
+ if (workerEntryPoints?.length) {
56
+ await esbuildWorkers(build, registry, define, workerEntryPoints, c.esbuild);
57
+ }
58
+ await rewriteWorkerUrls(build, registry);
59
+ // write out html output with rewritten hrefs
65
60
  await Promise.all(htmlEntrypoints.map(async (html) => {
66
- html.rewriteHrefs(mapInToOutHrefs);
67
- await html.writeTo(distDir);
61
+ await writeFile(join(build.dirs.buildDist, html.url, 'index.html'), html.output(registry));
62
+ }));
63
+ }
64
+ export async function rewriteWorkerUrls(build, registry) {
65
+ const workers = registry.workers();
66
+ if (!workers) {
67
+ return;
68
+ }
69
+ const dependentBundlePaths = workers.map(w => registry.mappedHref(w.dependentEntryPoint));
70
+ const bundleOutputs = {};
71
+ // collect all js file contents concurrently
72
+ const readingFiles = Promise.all(dependentBundlePaths.map(async (p) => {
73
+ bundleOutputs[p] = await readFile(join(build.dirs.projectRootAbs, build.dirs.buildDist, p), 'utf8');
74
+ }));
75
+ // build regex replacements during file reads
76
+ const rewriteChains = {};
77
+ for (const p of dependentBundlePaths)
78
+ rewriteChains[p] = [];
79
+ for (const w of workers) {
80
+ rewriteChains[registry.mappedHref(w.dependentEntryPoint)].push(s => s.replace(createWorkerRegex(w.workerUrlPlaceholder), `new Worker('${registry.mappedHref(w.workerEntryPoint)}')`));
81
+ }
82
+ // wait for file reads
83
+ await readingFiles;
84
+ // run rewrite regex chain and write back to dist
85
+ await Promise.all(Object.entries(bundleOutputs).map(async ([p, content]) => {
86
+ let result = content;
87
+ for (const rewriteFn of rewriteChains[p]) {
88
+ result = rewriteFn(result);
89
+ }
90
+ await writeFile(join(build.dirs.projectRootAbs, build.dirs.buildDist, p), result);
68
91
  }));
69
- return buildUrls;
92
+ }
93
+ export function createWorkerRegex(workerUrl) {
94
+ return new RegExp(`new(?:\\s|\\r?\\n)+Worker(?:\\s|\\r?\\n)*\\((?:\\s|\\r?\\n)*['"]${workerUrl}['"](?:\\s|\\r?\\n)*\\)`, 'g');
70
95
  }