rebuildjs 0.70.47 → 0.71.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rebuildjs",
3
- "version": "0.70.47",
3
+ "version": "0.71.1",
4
4
  "description": "Reactive esbuild...simple hackable alternative to vite for Multi Page Apps",
5
5
  "keywords": [
6
6
  "reactive",
@@ -51,6 +51,14 @@
51
51
  "types": "./server/index.d.ts",
52
52
  "default": "./server/index.js"
53
53
  },
54
+ "./server/export": {
55
+ "types": "./server/export/index.d.ts",
56
+ "default": "./server/export/index.js"
57
+ },
58
+ "./server/export/cloudflare": {
59
+ "types": "./server/export/cloudflare/index.d.ts",
60
+ "default": "./server/export/cloudflare/index.js"
61
+ },
54
62
  "./package.json": "./package.json"
55
63
  },
56
64
  "scripts": {
@@ -65,7 +73,6 @@
65
73
  },
66
74
  "dependencies": {
67
75
  "ctx-core": "*",
68
- "elysia": "^1.4.27",
69
76
  "fdir": "^6.5.0",
70
77
  "picomatch": "^4.0.3"
71
78
  },
@@ -75,6 +82,7 @@
75
82
  "devDependencies": {
76
83
  "@typescript-eslint/eslint-plugin": "^8.56.1",
77
84
  "@typescript-eslint/parser": "^8.56.1",
85
+ "bun-types": "^1.3.10",
78
86
  "c8": "^11.0.0",
79
87
  "eslint": "^10.0.2",
80
88
  "esmock": "^2.7.3",
@@ -29,6 +29,7 @@ export declare function rebuildjs__ready__wait(timeout?:number):rmemo__wait_ret_
29
29
  export declare function rebuildjs_browser__build(config?:rebuildjs_build_config_T):Promise<BuildContext|undefined>
30
30
  export declare function rebuildjs_server__build(config?:rebuildjs_build_config_T):Promise<BuildContext|undefined>
31
31
  export declare function server__external_(config?:Partial<BuildOptions>):Promise<string[]>
32
+ export declare function ts_resolve_node_modules_plugin_():Plugin
32
33
  export declare function rebuildjs_plugin_():Plugin
33
34
  export declare function cssBundle__annotate(cssBundle:string, suffix?:string):string
34
35
  export declare function server__metafile__update(
@@ -43,5 +44,9 @@ export type rebuildjs_build_config_T =
43
44
  Partial<BuildOptions>&{ rebuildjs?:rebuildjs_plugin_config_T }
44
45
  export type rebuildjs_plugin_config_T = {
45
46
  watch?:boolean
47
+ /** External directories to watch for file changes during dev mode.
48
+ * When a file in these directories changes, esbuild triggers a rebuild.
49
+ * Useful for watching source files from other monorepos (e.g., rappstack-dev). */
50
+ watch_dirs?:string[]
46
51
  }
47
52
  export type rebuildjs__ready__add__ready$__T = ctx__be_T<rmemo_T<boolean>, 'app'>
@@ -20,7 +20,7 @@ import {
20
20
  import { short_uuid_ } from 'ctx-core/uuid'
21
21
  import { build, context } from 'esbuild'
22
22
  import { fdir } from 'fdir'
23
- import { cp, link, mkdir, readFile, rm } from 'node:fs/promises'
23
+ import { cp, link, mkdir, readFile, rm, watch } from 'node:fs/promises'
24
24
  import { basename, dirname, extname, join, relative, resolve } from 'node:path'
25
25
  import {
26
26
  app_path_,
@@ -264,7 +264,7 @@ export async function rebuildjs_browser__build(config) {
264
264
  entryPoints.push(path)
265
265
  }
266
266
  const external = [dist_path_(app_ctx) + '/*', ...(esbuild__config.external ?? [])]
267
- const plugins = [rebuildjs_plugin_(), ...(esbuild__config.plugins ?? [])]
267
+ const plugins = [ts_resolve_node_modules_plugin_(), rebuildjs_plugin_(), ...(esbuild__config.plugins ?? [])]
268
268
  /** @type {import('esbuild').BuildOptions} */
269
269
  const esbuild_config = {
270
270
  entryNames: '[name]-[hash]',
@@ -290,6 +290,9 @@ export async function rebuildjs_browser__build(config) {
290
290
  if (rebuildjs?.watch ?? !is_prod_(app_ctx)) {
291
291
  const esbuild_ctx = await context(esbuild_config)
292
292
  await esbuild_ctx.watch()
293
+ if (rebuildjs?.watch_dirs?.length) {
294
+ watch_dirs__start(esbuild_ctx, rebuildjs.watch_dirs, 'browser')
295
+ }
293
296
  console.log('browser__build|watch')
294
297
  return esbuild_ctx
295
298
  } else {
@@ -326,7 +329,7 @@ export async function rebuildjs_server__build(config) {
326
329
  entryPoints.push(path)
327
330
  }
328
331
  const external = [dist_path_(app_ctx) + '/*', ...server__external_(esbuild__config)]
329
- const plugins = [rebuildjs_plugin_(), ...(esbuild__config.plugins || [])]
332
+ const plugins = [ts_resolve_node_modules_plugin_(), rebuildjs_plugin_(), ...(esbuild__config.plugins || [])]
330
333
  const esbuild_config = {
331
334
  entryNames: '[name]-[hash]',
332
335
  assetNames: '[name]-[hash]',
@@ -351,6 +354,9 @@ export async function rebuildjs_server__build(config) {
351
354
  if (rebuildjs?.watch ?? !is_prod_(app_ctx)) {
352
355
  const esbuild_ctx = await context(esbuild_config)
353
356
  await esbuild_ctx.watch()
357
+ if (rebuildjs?.watch_dirs?.length) {
358
+ watch_dirs__start(esbuild_ctx, rebuildjs.watch_dirs, 'server')
359
+ }
354
360
  console.log('server__build|watch')
355
361
  return esbuild_ctx
356
362
  } else {
@@ -361,6 +367,41 @@ export async function rebuildjs_server__build(config) {
361
367
  return undefined
362
368
  }
363
369
  }
370
+ /**
371
+ * Watch external directories and trigger esbuild rebuild on file changes.
372
+ * Used to watch source files from other monorepos (e.g., rappstack-dev)
373
+ * that aren't in the esbuild module graph.
374
+ * @param {import('esbuild').BuildContext} esbuild_ctx
375
+ * @param {string[]} dirs
376
+ * @param {string} label
377
+ */
378
+ function watch_dirs__start(esbuild_ctx, dirs, label) {
379
+ let rebuild_timer = null
380
+ for (const dir of dirs) {
381
+ ;(async ()=>{
382
+ try {
383
+ const watcher = watch(dir, { recursive: true })
384
+ for await (const event of watcher) {
385
+ if (!event.filename) continue
386
+ if (!/\.(ts|tsx|js|jsx|css)$/.test(event.filename)) continue
387
+ // Debounce: wait 100ms for batch changes
388
+ if (rebuild_timer) clearTimeout(rebuild_timer)
389
+ rebuild_timer = setTimeout(async ()=>{
390
+ rebuild_timer = null
391
+ console.log(`watch_dirs|${label}|rebuild`, event.filename)
392
+ try {
393
+ await esbuild_ctx.rebuild()
394
+ } catch (err) {
395
+ console.error(`watch_dirs|${label}|rebuild|error`, err.message)
396
+ }
397
+ }, 100)
398
+ }
399
+ } catch (err) {
400
+ console.error(`watch_dirs|${label}|watch|error`, dir, err.message)
401
+ }
402
+ })()
403
+ }
404
+ }
364
405
  /**
365
406
  * @param {rebuildjs_build_config_T}[config]
366
407
  * @returns {Promise<string[]>}
@@ -368,6 +409,32 @@ export async function rebuildjs_server__build(config) {
368
409
  export function server__external_(config) {
369
410
  return ['bun', 'node_modules/*', ...(config.external || [])]
370
411
  }
412
+ /**
413
+ * esbuild plugin that resolves `.ts` files from node_modules package exports.
414
+ * esbuild 0.27+ blocks `.ts` resolution from node_modules by default.
415
+ * This plugin intercepts bare specifier imports (e.g. `@rappstack/ui--server/css`)
416
+ * and resolves them through bun's module resolution, allowing `.ts` targets.
417
+ * @returns {import('esbuild').Plugin}
418
+ */
419
+ export function ts_resolve_node_modules_plugin_() {
420
+ return {
421
+ name: 'ts_resolve_node_modules',
422
+ setup(build) {
423
+ const absWorkingDir = build.initialOptions.absWorkingDir || process.cwd()
424
+ build.onResolve({ filter: /^[^./]/ }, async (args)=>{
425
+ if (args.pluginData?.fromTsResolve) return
426
+ try {
427
+ const resolved = Bun.resolveSync(args.path, absWorkingDir)
428
+ if (resolved && (resolved.endsWith('.ts') || resolved.endsWith('.tsx'))) {
429
+ return { path: resolved }
430
+ }
431
+ } catch {
432
+ // Let esbuild handle the resolution
433
+ }
434
+ })
435
+ }
436
+ }
437
+ }
371
438
  /**
372
439
  * @returns {import('esbuild').Plugin}
373
440
  * @private
@@ -0,0 +1,37 @@
1
+ /// <reference types="bun-types" />
2
+ import type { static_export_config_T, static_export_result_T } from '../index.js'
3
+ export declare function cloudflare_export_(
4
+ config:cloudflare_export_config_T,
5
+ static_export_:(config:static_export_config_T, app__start:(app?:any)=>Promise<any>)=>Promise<static_export_result_T>,
6
+ app__start:(app?:any)=>Promise<any>
7
+ ):Promise<cloudflare_export_result_T>
8
+ export declare function worker_entry__generate_(
9
+ dynamic_routes:route_handler_T[]
10
+ ):string
11
+ export declare function wrangler_toml__generate_(
12
+ overrides:Partial<wrangler_config_T>,
13
+ out_dir?:string
14
+ ):string
15
+ export type cloudflare_export_config_T =
16
+ & static_export_config_T
17
+ & {
18
+ dynamic_routes?:route_handler_T[]
19
+ worker_out?:string
20
+ wrangler?:Partial<wrangler_config_T>
21
+ }
22
+ export type cloudflare_export_result_T =
23
+ & static_export_result_T
24
+ & {
25
+ worker_entry_path?:string
26
+ wrangler_path:string
27
+ }
28
+ export type route_handler_T = {
29
+ pattern:string
30
+ handler:string
31
+ }
32
+ export type wrangler_config_T = {
33
+ name:string
34
+ compatibility_date:string
35
+ vars?:Record<string, string>
36
+ routes?:{ pattern:string, zone_name?:string }[]
37
+ }
@@ -0,0 +1,108 @@
1
+ /// <reference types="bun-types" />
2
+ /// <reference types="./index.d.ts" />
3
+ import { build } from 'esbuild'
4
+ import { mkdir, writeFile } from 'node:fs/promises'
5
+ import { join, resolve } from 'node:path'
6
+ /**
7
+ * @param {cloudflare_export_config_T} config
8
+ * @param {(config:import('../index.js').static_export_config_T, app__start:(app?:any)=>Promise<any>)=>Promise<import('../index.js').static_export_result_T>} static_export_
9
+ * @param {(app?:any)=>Promise<any>} app__start
10
+ * @returns {Promise<cloudflare_export_result_T>}
11
+ */
12
+ export async function cloudflare_export_(config, static_export_, app__start) {
13
+ const {
14
+ dynamic_routes = [],
15
+ worker_out = 'dist/worker',
16
+ wrangler = {},
17
+ ...static_config
18
+ } = config
19
+ // 1. Run static export
20
+ const static_result = await static_export_(static_config, app__start)
21
+ // 2. Generate and bundle worker if dynamic routes exist
22
+ let worker_entry_path
23
+ if (dynamic_routes.length > 0) {
24
+ const entry_source = worker_entry__generate_(dynamic_routes)
25
+ const generated_path = join(worker_out, '_worker.src.js')
26
+ await mkdir(worker_out, { recursive: true })
27
+ await writeFile(generated_path, entry_source)
28
+ // Bundle with esbuild for Cloudflare Workers runtime
29
+ worker_entry_path = join(worker_out, '_worker.js')
30
+ await build({
31
+ entryPoints: [generated_path],
32
+ bundle: true,
33
+ format: 'esm',
34
+ target: 'es2022',
35
+ platform: 'browser',
36
+ outfile: worker_entry_path,
37
+ minify: true,
38
+ })
39
+ console.info(`[cloudflare_export] worker bundled: ${worker_entry_path}`)
40
+ }
41
+ // 3. Generate wrangler.toml
42
+ const wrangler_config = wrangler_toml__generate_(wrangler, static_config.out_dir)
43
+ const wrangler_path = 'wrangler.toml'
44
+ await writeFile(wrangler_path, wrangler_config)
45
+ console.info(`[cloudflare_export] wrote ${wrangler_path}`)
46
+ return {
47
+ ...static_result,
48
+ worker_entry_path,
49
+ wrangler_path,
50
+ }
51
+ }
52
+ /**
53
+ * @param {route_handler_T[]} dynamic_routes
54
+ * @returns {string}
55
+ */
56
+ export function worker_entry__generate_(dynamic_routes) {
57
+ const imports = []
58
+ const route_checks = []
59
+ for (let i = 0; i < dynamic_routes.length; i++) {
60
+ const { pattern, handler } = dynamic_routes[i]
61
+ const handler_name = `handler_${i}`
62
+ imports.push(`import ${handler_name} from '${resolve(handler)}'`)
63
+ if (pattern.endsWith('/*')) {
64
+ const prefix = pattern.slice(0, -2)
65
+ route_checks.push(
66
+ `\tif (url.pathname.startsWith('${prefix}')) return ${handler_name}(request, env, ctx)`)
67
+ } else {
68
+ route_checks.push(
69
+ `\tif (url.pathname === '${pattern}') return ${handler_name}(request, env, ctx)`)
70
+ }
71
+ }
72
+ return `${imports.join('\n')}
73
+
74
+ export default {
75
+ async fetch(request, env, ctx) {
76
+ const url = new URL(request.url)
77
+ ${route_checks.join('\n')}
78
+ return env.ASSETS.fetch(request)
79
+ }
80
+ }
81
+ `
82
+ }
83
+ /**
84
+ * @param {Partial<wrangler_config_T>} overrides
85
+ * @param {string} [out_dir]
86
+ * @returns {string}
87
+ */
88
+ export function wrangler_toml__generate_(overrides, out_dir = 'dist/browser') {
89
+ const name = overrides.name || 'app'
90
+ const compatibility_date = overrides.compatibility_date || new Date().toISOString().slice(0, 10)
91
+ let toml = `name = "${name}"
92
+ compatibility_date = "${compatibility_date}"
93
+ pages_build_output_dir = "${out_dir}"
94
+ `
95
+ if (overrides.vars) {
96
+ toml += '\n[vars]\n'
97
+ for (const [key, val] of Object.entries(overrides.vars)) {
98
+ toml += `${key} = "${val}"\n`
99
+ }
100
+ }
101
+ if (overrides.routes) {
102
+ for (const route of overrides.routes) {
103
+ toml += `\n[[routes]]\npattern = "${route.pattern}"\n`
104
+ if (route.zone_name) toml += `zone_name = "${route.zone_name}"\n`
105
+ }
106
+ }
107
+ return toml
108
+ }
@@ -0,0 +1,29 @@
1
+ /// <reference types="bun-types" />
2
+ export declare function static_export_(
3
+ config:static_export_config_T,
4
+ app__start:(app?:any)=>Promise<any>
5
+ ):Promise<static_export_result_T>
6
+ export declare function static_export__file_path_(
7
+ route:string,
8
+ out_dir:string,
9
+ content_type?:string
10
+ ):string
11
+ export type static_export_config_T = {
12
+ routes?:string[]
13
+ site_url:string
14
+ out_dir?:string
15
+ base_url?:string
16
+ server_import?:string
17
+ app?:any
18
+ sitemap?:boolean
19
+ extra_routes?:string[]
20
+ url_rewrite?:boolean
21
+ incremental?:boolean
22
+ manifest?:boolean
23
+ clean?:boolean
24
+ on_export?:(route:string, file:string)=>void
25
+ }
26
+ export type static_export_result_T = {
27
+ exported:string[]
28
+ errors:string[]
29
+ }
@@ -0,0 +1,168 @@
1
+ /// <reference types="bun-types" />
2
+ /// <reference types="./index.d.ts" />
3
+ import { mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises'
4
+ import { dirname, extname, join } from 'node:path'
5
+ import { port_ } from '../app/index.js'
6
+ import { app_ctx } from '../ctx/index.js'
7
+ /**
8
+ * @param {static_export_config_T} config
9
+ * @param {(app?:any)=>Promise<any>} app__start
10
+ * @returns {Promise<static_export_result_T>}
11
+ */
12
+ export async function static_export_(config, app__start) {
13
+ const {
14
+ site_url,
15
+ out_dir = 'dist/browser',
16
+ server_import = './dist/server/index.js',
17
+ sitemap = true,
18
+ url_rewrite = true,
19
+ incremental = false,
20
+ manifest: use_manifest = incremental,
21
+ clean = false,
22
+ on_export,
23
+ } = config
24
+ let { routes = [], extra_routes = [] } = config
25
+ const site_origin = site_url.replace(/\/$/, '')
26
+ const exported = []
27
+ const errors = []
28
+ let app = config.app
29
+ let we_started = false
30
+ let base_url = config.base_url
31
+ try {
32
+ if (base_url) {
33
+ // Connect to already-running external server
34
+ base_url = base_url.replace(/\/$/, '')
35
+ console.info(`[static_export] using external server: ${base_url}`)
36
+ } else if (!app) {
37
+ // Start server internally
38
+ const mod = await import(server_import)
39
+ app = typeof mod.default === 'function' ? await mod.default() : mod.default
40
+ await app__start(app)
41
+ we_started = true
42
+ base_url = `http://localhost:${port_(app_ctx)}`
43
+ console.info(`[static_export] server running on ${base_url}`)
44
+ } else {
45
+ base_url = `http://localhost:${port_(app_ctx)}`
46
+ console.info(`[static_export] using provided app on ${base_url}`)
47
+ }
48
+ console.info(`[static_export] site_url: ${site_origin}`)
49
+ // Clean output directory if requested
50
+ if (clean) {
51
+ await rm(out_dir, { recursive: true, force: true })
52
+ }
53
+ // Discover routes from sitemap
54
+ if (sitemap) {
55
+ try {
56
+ const res = await fetch(`${base_url}/sitemap.xml`)
57
+ if (res.ok) {
58
+ const xml = await res.text()
59
+ for (const m of xml.matchAll(/<loc>https?:\/\/[^<]+<\/loc>/g)) {
60
+ const url = m[0].replace(/<\/?loc>/g, '')
61
+ const path = new URL(url).pathname
62
+ const route = path === '' ? '/' : path
63
+ if (!routes.includes(route)) {
64
+ routes.push(route)
65
+ }
66
+ }
67
+ console.info(`[static_export] discovered ${routes.length} routes from sitemap`)
68
+ }
69
+ } catch {
70
+ console.warn('[static_export] sitemap fetch failed, using explicit routes only')
71
+ }
72
+ }
73
+ // Load previous manifest for stale file cleanup
74
+ const manifest_path = join(out_dir, '.export-manifest.json')
75
+ let prev_manifest = []
76
+ if (use_manifest) {
77
+ try {
78
+ prev_manifest = JSON.parse(await readFile(manifest_path, 'utf-8'))
79
+ } catch {
80
+ // No previous manifest
81
+ }
82
+ }
83
+ // Merge routes and extra_routes
84
+ const all_routes = [...routes, ...extra_routes]
85
+ // Export each route (streaming — write as we go)
86
+ for (const route of all_routes) {
87
+ const url = base_url + route
88
+ try {
89
+ const res = await fetch(url)
90
+ if (!res.ok) {
91
+ console.error(`[static_export] ${route} -> ${res.status} ${res.statusText}`)
92
+ errors.push(route)
93
+ continue
94
+ }
95
+ const content_type = res.headers.get('content-type') ?? ''
96
+ let body = await res.text()
97
+ if (url_rewrite) {
98
+ body = body.replaceAll(base_url, site_origin)
99
+ }
100
+ const file_path = static_export__file_path_(route, out_dir, content_type)
101
+ // Incremental: skip if content unchanged
102
+ if (incremental) {
103
+ try {
104
+ const existing = await readFile(file_path, 'utf-8')
105
+ if (existing === body) {
106
+ exported.push(file_path)
107
+ continue
108
+ }
109
+ } catch {
110
+ // File doesn't exist yet — write it
111
+ }
112
+ }
113
+ await mkdir(dirname(file_path), { recursive: true })
114
+ await writeFile(file_path, body)
115
+ exported.push(file_path)
116
+ on_export?.(route, file_path)
117
+ console.info(`[static_export] ${route} -> ${file_path} (${body.length} bytes)`)
118
+ } catch (err) {
119
+ console.error(`[static_export] ${route} -> ${err.message}`)
120
+ errors.push(route)
121
+ }
122
+ }
123
+ // Manifest: write new manifest and delete stale files
124
+ if (use_manifest) {
125
+ await mkdir(dirname(manifest_path), { recursive: true })
126
+ await writeFile(manifest_path, JSON.stringify(exported, null, '\t'))
127
+ const stale = prev_manifest.filter(f=>!exported.includes(f))
128
+ for (const file of stale) {
129
+ try {
130
+ await unlink(file)
131
+ console.info(`[static_export] deleted stale: ${file}`)
132
+ } catch {
133
+ // Already gone
134
+ }
135
+ }
136
+ }
137
+ if (errors.length > 0) {
138
+ console.warn(`[static_export] completed with ${errors.length} error(s)`)
139
+ } else {
140
+ console.info(`[static_export] exported ${exported.length} files`)
141
+ }
142
+ } finally {
143
+ if (we_started && app?.stop) {
144
+ app.stop()
145
+ }
146
+ }
147
+ return { exported, errors }
148
+ }
149
+ /**
150
+ * @param {string} route
151
+ * @param {string} out_dir
152
+ * @param {string} [content_type]
153
+ * @returns {string}
154
+ */
155
+ export function static_export__file_path_(route, out_dir, content_type) {
156
+ const ext = extname(route)
157
+ // If the server responded with HTML content-type, treat as HTML page
158
+ const is_html = content_type ? content_type.includes('text/html') : !ext || ext === '.html'
159
+ if (ext && !is_html) {
160
+ // Raw file (robots.txt, sitemap.xml, rss.xml, etc.)
161
+ return join(out_dir, route)
162
+ }
163
+ // HTML route -> clean URL directory structure
164
+ if (route === '/') {
165
+ return join(out_dir, 'index.html')
166
+ }
167
+ return join(out_dir, route, 'index.html')
168
+ }
package/server/index.d.ts CHANGED
@@ -9,3 +9,5 @@ export * from './metafile_l0/index.js'
9
9
  export * from './middleware/index.js'
10
10
  export * from './rebuildjs_browser/index.js'
11
11
  export * from './rebuildjs_server/index.js'
12
+ export * from './route/index.js'
13
+ export * from './static/index.js'
package/server/index.js CHANGED
@@ -9,3 +9,5 @@ export * from './metafile_l0/index.js'
9
9
  export * from './middleware/index.js'
10
10
  export * from './rebuildjs_browser/index.js'
11
11
  export * from './rebuildjs_server/index.js'
12
+ export * from './route/index.js'
13
+ export * from './static/index.js'
@@ -0,0 +1,12 @@
1
+ /// <reference lib="dom" />
2
+ import type { middleware_ctx_T, request_ctx_T } from '../ctx/index.js'
3
+ export declare function html_route_<framework_ctx_T>(
4
+ middleware_ctx:middleware_ctx_T,
5
+ page_:($p:{ ctx:request_ctx_T })=>({ toString():string }|ReadableStream<string>),
6
+ request_ctx__ensure:(middleware_ctx:middleware_ctx_T, framework_ctx:framework_ctx_T)=>request_ctx_T,
7
+ response_init?:ResponseInit
8
+ ):(framework_ctx:framework_ctx_T)=>Response
9
+ export declare function html_response__new(
10
+ html_OR_stream:string|ReadableStream,
11
+ response_init?:ResponseInit
12
+ ):Response
@@ -0,0 +1,52 @@
1
+ /// <reference types="./index.d.ts" />
2
+ // TODO: use built-in TextEncoderStream when bunjs implements TextEncoderStream
3
+ // See https://github.com/oven-sh/bun/issues/5648
4
+ // See https://github.com/oven-sh/bun/issues/159
5
+ import { TextEncoderStream } from 'ctx-core/stream'
6
+ import { request_ctx__new } from '../ctx/index.js'
7
+ /**
8
+ * @param {middleware_ctx_T}middleware_ctx
9
+ * @param {($p:{ ctx:request_ctx_T })=>(string|ReadableStream<string|Uint8Array>)}page_
10
+ * @param {(middleware_ctx:middleware_ctx_T, framework_ctx:any)=>request_ctx_T}request_ctx__ensure
11
+ * @param {ResponseInit}[response_init]
12
+ * @returns {(framework_ctx:any)=>Response}
13
+ */
14
+ export function html_route_(
15
+ middleware_ctx,
16
+ page_,
17
+ request_ctx__ensure,
18
+ response_init
19
+ ) {
20
+ return framework_ctx=>
21
+ html_response__new(
22
+ page_({
23
+ ctx: request_ctx__ensure(middleware_ctx, framework_ctx)
24
+ }),
25
+ response_init)
26
+ }
27
+ /**
28
+ * @param {string|ReadableStream}html_OR_stream
29
+ * @param {ResponseInit}[response_init]
30
+ * @returns {Response}
31
+ */
32
+ export function html_response__new(
33
+ html_OR_stream,
34
+ response_init
35
+ ) {
36
+ const headers = new Headers(response_init?.headers)
37
+ headers.set('Content-Type', 'text/html;charset=UTF-8')
38
+ return new Response(
39
+ html_OR_stream.pipeTo
40
+ ? html_OR_stream.pipeThrough(new TextEncoderStream())
41
+ : new ReadableStream({
42
+ start(controller) {
43
+ controller.enqueue('' + html_OR_stream)
44
+ controller.close()
45
+ }
46
+ }),
47
+ {
48
+ ...(response_init ?? {}),
49
+ headers
50
+ }
51
+ )
52
+ }
@@ -0,0 +1,11 @@
1
+ /// <reference types="bun-types" />
2
+ export declare function static_middleware__routes_(
3
+ config?:static_middleware__config_T
4
+ ):Promise<static_middleware__route_T[]>
5
+ export type static_middleware__config_T = {
6
+ headers_?:(url_path:string, content_type:string, path:string)=>Record<string, string>
7
+ }
8
+ export type static_middleware__route_T = {
9
+ url_path:string
10
+ handler:()=>Response
11
+ }
@@ -0,0 +1,37 @@
1
+ /// <reference types="bun-types" />
2
+ /// <reference types="./index.d.ts" />
3
+ import { file, Glob } from 'bun'
4
+ import { ext_R_mime } from 'ctx-core/http'
5
+ import { extname, join } from 'node:path'
6
+ import { browser_path_ } from '../app/index.js'
7
+ import { app_ctx } from '../ctx/index.js'
8
+ /**
9
+ * @param {static_middleware__config_T}[config]
10
+ * @returns {Promise<{get:(path:string, handler:Function)=>void}>}
11
+ */
12
+ export async function static_middleware__routes_(config) {
13
+ const routes = []
14
+ const glob = new Glob('**')
15
+ for await (const relative_path of glob.scan(browser_path_(app_ctx))) {
16
+ const url_path = join('/', relative_path)
17
+ const content_type = ext_R_mime[extname(relative_path)] ?? 'text/plain'
18
+ const path = join(browser_path_(app_ctx), relative_path)
19
+ const headers = (config?.headers_ ?? (()=>({})))(
20
+ url_path,
21
+ content_type,
22
+ path)
23
+ routes.push({
24
+ url_path,
25
+ handler: ()=>{
26
+ const file_ref = file(path)
27
+ return new Response(file_ref.size ? file_ref.stream() : '', {
28
+ headers: {
29
+ 'Content-Type': content_type,
30
+ ...headers,
31
+ }
32
+ })
33
+ }
34
+ })
35
+ }
36
+ return routes
37
+ }