rebuildjs 0.70.46 → 0.71.0

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.46",
3
+ "version": "0.71.0",
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",
@@ -276,6 +276,7 @@ export async function rebuildjs_browser__build(config) {
276
276
  sourcemap: 'external',
277
277
  loader: default_loader,
278
278
  publicPath: '/',
279
+ write: true,
279
280
  ...esbuild__config,
280
281
  entryPoints,
281
282
  format: 'esm',
@@ -292,7 +293,10 @@ export async function rebuildjs_browser__build(config) {
292
293
  console.log('browser__build|watch')
293
294
  return esbuild_ctx
294
295
  } else {
295
- await build(esbuild_config)
296
+ const result = await build(esbuild_config)
297
+ if (!result.metafile) {
298
+ throw new Error('rebuildjs_browser__build: esbuild.build() produced no metafile')
299
+ }
296
300
  return undefined
297
301
  }
298
302
  }
@@ -333,6 +337,7 @@ export async function rebuildjs_server__build(config) {
333
337
  sourcemap: 'external',
334
338
  loader: default_loader,
335
339
  publicPath: '/',
340
+ write: true,
336
341
  ...esbuild__config,
337
342
  entryPoints,
338
343
  format: 'esm',
@@ -349,7 +354,10 @@ export async function rebuildjs_server__build(config) {
349
354
  console.log('server__build|watch')
350
355
  return esbuild_ctx
351
356
  } else {
352
- await build(esbuild_config)
357
+ const result = await build(esbuild_config)
358
+ if (!result.metafile) {
359
+ throw new Error('rebuildjs_server__build: esbuild.build() produced no metafile')
360
+ }
353
361
  return undefined
354
362
  }
355
363
  }
@@ -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
+ }