rhonojs 0.1.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.
@@ -0,0 +1 @@
1
+ export * from 'rebuildjs/browser'
@@ -0,0 +1 @@
1
+ export * from 'rebuildjs/browser'
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export {}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "rhonojs",
3
+ "version": "0.1.0",
4
+ "description": "Reactive Web app server focusing on MPAs with a simple server route & browser build api...uses Hono, ESBuild, rmemo, & ctx-core",
5
+ "keywords": [
6
+ "reactive",
7
+ "web app",
8
+ "web server",
9
+ "hono",
10
+ "cloudflare workers",
11
+ "esbuild",
12
+ "rmemo",
13
+ "ctx-core"
14
+ ],
15
+ "homepage": "https://github.com/btakita/rhonojs#readme",
16
+ "license": "Apache-2.0",
17
+ "author": {
18
+ "name": "Brian Takita",
19
+ "url": "https://briantakita.me",
20
+ "email": "info+rhonojs@briantakita.me"
21
+ },
22
+ "type": "module",
23
+ "files": [
24
+ "*.d.ts",
25
+ "*.js",
26
+ "*.json",
27
+ "browser",
28
+ "server",
29
+ "types"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "default": "./index.js"
35
+ },
36
+ "./browser": {
37
+ "types": "./browser/index.d.ts",
38
+ "default": "./browser/index.js"
39
+ },
40
+ "./types": {
41
+ "types": "./types/index.d.ts"
42
+ },
43
+ "./server": {
44
+ "types": "./server/index.d.ts",
45
+ "default": "./server/index.js"
46
+ },
47
+ "./server/export": {
48
+ "types": "./server/export/index.d.ts",
49
+ "default": "./server/export/index.js"
50
+ },
51
+ "./server/export/cloudflare": {
52
+ "types": "./server/export/cloudflare/index.d.ts",
53
+ "default": "./server/export/cloudflare/index.js"
54
+ },
55
+ "./package.json": "./package.json"
56
+ },
57
+ "dependencies": {
58
+ "ctx-core": "*",
59
+ "hono": "^4.0.0",
60
+ "rebuildjs": "*"
61
+ },
62
+ "peerDependencies": {
63
+ "esbuild": "^0.27.3"
64
+ },
65
+ "sideEffects": false
66
+ }
@@ -0,0 +1,15 @@
1
+ import type { ctx__be_T, ctx__get_T, ctx__set_T, sig_T } from 'ctx-core/rmemo'
2
+ import type { Hono } from 'hono'
3
+ export declare const app$_:ctx__be_T<sig_T<Hono|undefined>, 'app'>
4
+ export declare const app_:ctx__get_T<Hono|undefined, 'app'>
5
+ export declare const app__set:ctx__set_T<Hono|undefined, 'app'>
6
+ export declare const server_entry__relative_path$_:ctx__be_T<sig_T<string|undefined>, 'app'>
7
+ export declare const server_entry__relative_path_:ctx__get_T<string|undefined, 'app'>
8
+ export declare const server_entry__output__relative_path$_:ctx__be_T<sig_T<string|undefined>, 'app'>
9
+ export declare const server_entry__output__relative_path_:ctx__get_T<string|undefined, 'app'>
10
+ export declare const server_entry__output__path$_:ctx__be_T<sig_T<string|undefined>, 'app'>
11
+ export declare const server_entry__output__path_:ctx__get_T<string|undefined, 'app'>
12
+ export declare const server_entry__output__link__path$_:ctx__be_T<sig_T<string|undefined>, 'app'>
13
+ export declare const server_entry__output__link__path_:ctx__get_T<string|undefined, 'app'>
14
+ export declare function app__attach(app?:Hono):Promise<Hono>
15
+ export declare function app__start(app?:Hono):Promise<Hono>
@@ -0,0 +1,175 @@
1
+ import { file_exists_, file_exists__waitfor } from 'ctx-core/fs'
2
+ import {
3
+ calling,
4
+ memo_,
5
+ ns_id_be_memo_pair_,
6
+ ns_id_be_sig_triple_,
7
+ nullish__none_,
8
+ off,
9
+ promise__cancel,
10
+ promise__cancel__throw,
11
+ ref__bind,
12
+ rmemo__wait,
13
+ run,
14
+ tup
15
+ } from 'ctx-core/rmemo'
16
+ import { Hono } from 'hono'
17
+ import { dirname, join } from 'node:path'
18
+ import {
19
+ app__relative_path_,
20
+ app_ctx,
21
+ browser__metafile$_,
22
+ build_id_,
23
+ cwd_,
24
+ metafile__wait,
25
+ port_,
26
+ server__metafile_,
27
+ server__output_,
28
+ server__output__relative_path_,
29
+ server__output__relative_path_M_middleware_ctx_
30
+ } from 'rebuildjs/server'
31
+ export const [
32
+ app$_,
33
+ app_,
34
+ app__set
35
+ ] = ns_id_be_sig_triple_(
36
+ 'app',
37
+ 'app',
38
+ ()=>undefined)
39
+ export const [
40
+ server_entry__relative_path$_,
41
+ server_entry__relative_path_,
42
+ ] = ns_id_be_memo_pair_(
43
+ 'app',
44
+ 'server_entry__relative_path',
45
+ ctx=>
46
+ join(app__relative_path_(ctx), 'index.ts'))
47
+ export const [
48
+ server_entry__output__relative_path$_,
49
+ server_entry__output__relative_path_,
50
+ ] = ns_id_be_memo_pair_(
51
+ 'app',
52
+ 'server_entry__output__relative_path',
53
+ ctx=>
54
+ nullish__none_(tup(server__metafile_(ctx), server_entry__relative_path_(ctx)),
55
+ (server__metafile, server_entry__relative_path)=>{
56
+ const { outputs } = server__metafile
57
+ for (const output_path in outputs) {
58
+ const output = outputs[output_path]
59
+ if (output.entryPoint === server_entry__relative_path) return output_path
60
+ }
61
+ }))
62
+ export const [
63
+ server_entry__output__path$_,
64
+ server_entry__output__path_,
65
+ ] = ns_id_be_memo_pair_(
66
+ 'app',
67
+ 'server_entry__output__path',
68
+ ctx=>
69
+ nullish__none_(tup(cwd_(ctx), server_entry__output__relative_path_(ctx)),
70
+ (cwd, server_entry__output__relative_path)=>
71
+ join(cwd, server_entry__output__relative_path)))
72
+ export const [
73
+ server_entry__output__link__path$_,
74
+ server_entry__output__link__path_,
75
+ ] = ns_id_be_memo_pair_(
76
+ 'app',
77
+ 'server_entry__output__link__path',
78
+ ctx=>
79
+ nullish__none_([server_entry__output__path_(ctx)],
80
+ server_entry__output__path=>
81
+ join(dirname(server_entry__output__path), 'index.js')))
82
+ /**
83
+ * @param {Hono}[app]
84
+ * @returns {Promise<Hono>}
85
+ */
86
+ export async function app__attach(app) {
87
+ await metafile__wait(app_ctx)
88
+ const neq_undefined = val=>val !== undefined
89
+ await rmemo__wait(browser__metafile$_(app_ctx), neq_undefined)
90
+ const app$ = app$__new()
91
+ const val = await rmemo__wait(app$, app=>app, 10_000)
92
+ if (val instanceof Error) {
93
+ throw val
94
+ }
95
+ return val
96
+ function app$__new() {
97
+ return calling(memo_(app$=>{
98
+ app ??= new Hono()
99
+ app._rhonojs = 1
100
+ const build_id = build_id_(app_ctx)
101
+ const server__output__relative_path_M_middleware_ctx = server__output__relative_path_M_middleware_ctx_(app_ctx)
102
+ const middleware_a1 = []
103
+ run(async ()=>{
104
+ for (
105
+ const middleware_ctx of server__output__relative_path_M_middleware_ctx.values()
106
+ ) {
107
+ const output = server__output_(middleware_ctx)
108
+ if (!output) {
109
+ return
110
+ }
111
+ if (output.entryPoint !== server_entry__relative_path_(app_ctx)) {
112
+ await file_exists__waitfor(async ()=>{
113
+ const path = join(cwd_(app_ctx), server__output__relative_path_(middleware_ctx))
114
+ if (!await cmd(file_exists_(path))) {
115
+ return false
116
+ }
117
+ const server__middleware_ =
118
+ await cmd(import(path).then(mod=>mod.default))
119
+ if (server__middleware_) {
120
+ middleware_a1.push(server__middleware_(middleware_ctx))
121
+ } else {
122
+ console.warn('module ' + path + ' does not export a default function')
123
+ return false
124
+ }
125
+ return true
126
+ })
127
+ }
128
+ }
129
+ for (const middleware of middleware_a1) {
130
+ app.use(middleware)
131
+ }
132
+ app$.set(app)
133
+ }).catch(err=>{
134
+ app$.set(err)
135
+ })
136
+ return app$.val
137
+ async function cmd(promise) {
138
+ if (cancel_()) promise__cancel__throw(promise)
139
+ if (!promise) return promise
140
+ ref__bind(promise, calling(memo_(rhonojs_cancel$=>{
141
+ if (cancel_()) {
142
+ promise__cancel(promise)
143
+ off(rhonojs_cancel$)
144
+ }
145
+ })))
146
+ const ret = await promise
147
+ if (cancel_()) promise__cancel__throw(promise)
148
+ return ret
149
+ }
150
+ function cancel_() {
151
+ return (
152
+ build_id_(app_ctx) !== build_id
153
+ || server__output__relative_path_M_middleware_ctx_(app_ctx) !== server__output__relative_path_M_middleware_ctx
154
+ )
155
+ }
156
+ }))
157
+ }
158
+ }
159
+ /**
160
+ * @param {Hono}[app]
161
+ * @returns {Promise<Hono>}
162
+ */
163
+ export async function app__start(app) {
164
+ if (!app?._rhonojs) {
165
+ app = await app__attach(app)
166
+ }
167
+ app__set(app_ctx, app)
168
+ const port = port_(app_ctx)
169
+ Bun.serve({
170
+ port,
171
+ fetch: app.fetch,
172
+ })
173
+ console.info(`server started on port ${port}`)
174
+ return app
175
+ }
@@ -0,0 +1,23 @@
1
+ import type { ctx__be_T, ctx__get_T, ctx__set_T, sig_T } from 'ctx-core/rmemo'
2
+ import type { BuildContext, Plugin } from 'esbuild'
3
+ import type { rebuildjs_build_config_T } from 'rebuildjs/server'
4
+ export declare const rhonojs__build_id$_:ctx__be_T<sig_T<string>, 'app'>
5
+ export declare const rhonojs__build_id_:ctx__get_T<string, 'app'>
6
+ export declare const rhonojs__build_id__set:ctx__set_T<string, 'app'>
7
+ export declare const rhonojs__ready$_:ctx__be_T<sig_T<boolean>, 'app'>
8
+ export declare const rhonojs__ready_:ctx__get_T<boolean, 'app'>
9
+ export declare function rhonojs__ready__wait(timeout?:number):Promise<void>
10
+ export declare function rhonojs_browser__build(
11
+ config?:rhonojs__build_config_T
12
+ ):Promise<BuildContext>
13
+ export declare function rhonojs_server__build(
14
+ config?:rhonojs__build_config_T
15
+ ):Promise<BuildContext>
16
+ export declare function rhonojs_plugin_(config?:rhonojs_plugin_config_T):Plugin
17
+ export type rhonojs__build_config_T =
18
+ & rebuildjs_build_config_T
19
+ & { rhonojs?:rhonojs_plugin_config_T }
20
+ export type rhonojs_plugin_config_T = {
21
+ server_entry?:string
22
+ app__start?:boolean
23
+ }
@@ -0,0 +1,202 @@
1
+ /// <reference types="esbuild" />
2
+ /// <reference types="./index.d.ts" />
3
+ import { file_exists_, file_exists__waitfor } from 'ctx-core/fs'
4
+ import {
5
+ calling,
6
+ Cancel,
7
+ ns_id_be,
8
+ ns_id_be_memo_pair_,
9
+ ns_id_be_sig_triple_,
10
+ nullish__none_,
11
+ promise__cancel,
12
+ promise__cancel__throw,
13
+ ref__bind,
14
+ run
15
+ } from 'ctx-core/rmemo'
16
+ import { Hono } from 'hono'
17
+ import { link, rm } from 'node:fs/promises'
18
+ import { join } from 'node:path'
19
+ import {
20
+ app_ctx,
21
+ app_path_,
22
+ build_id_,
23
+ memo_,
24
+ metafile__build_id_,
25
+ off,
26
+ port_,
27
+ rebuildjs__esbuild__done_,
28
+ rebuildjs__ready_,
29
+ rebuildjs_browser__build,
30
+ rebuildjs_server__build,
31
+ rmemo__wait
32
+ } from 'rebuildjs/server'
33
+ import { app_, app__start, server_entry__output__link__path_, server_entry__output__path_ } from '../app/index.js'
34
+ export const [
35
+ rhonojs__build_id$_,
36
+ rhonojs__build_id_,
37
+ rhonojs__build_id__set,
38
+ ] = ns_id_be_sig_triple_(
39
+ 'app',
40
+ 'rhonojs_plugin__build_id',
41
+ ()=>undefined)
42
+ export const [
43
+ rhonojs__ready$_,
44
+ rhonojs__ready_,
45
+ ] = ns_id_be_memo_pair_(
46
+ 'app',
47
+ 'rhonojs__ready',
48
+ ctx=>
49
+ !!(
50
+ build_id_(ctx)
51
+ && rebuildjs__ready_(ctx)
52
+ && build_id_(ctx) === metafile__build_id_(ctx)
53
+ && build_id_(ctx) === rhonojs__build_id_(ctx)))
54
+ /**
55
+ * @param {number}[timeout]
56
+ * @returns {Promise<void>}
57
+ */
58
+ export function rhonojs__ready__wait(timeout) {
59
+ return rmemo__wait(
60
+ rhonojs__ready$_(app_ctx),
61
+ ready=>ready,
62
+ timeout ?? 5000)
63
+ }
64
+ /**
65
+ * @param {rhonojs__build_config_T}[config]
66
+ */
67
+ export function rhonojs_browser__build(config) {
68
+ const {
69
+ rhonojs,
70
+ ...rebuildjs__config
71
+ } = config ?? {}
72
+ return rebuildjs_browser__build(rebuildjs__config)
73
+ }
74
+ /**
75
+ * @param {rhonojs__build_config_T}[config]
76
+ * @returns {Promise<void>}
77
+ */
78
+ export function rhonojs_server__build(config) {
79
+ const {
80
+ rhonojs,
81
+ ...rebuildjs__config
82
+ } = config ?? {}
83
+ const plugins = [rhonojs_plugin_(rhonojs), ...(config?.plugins ?? [])]
84
+ const entryPoints = config?.entryPoints ?? []
85
+ const server_entry = rhonojs?.server_entry ?? join(app_path_(app_ctx), 'index.ts')
86
+ entryPoints.push({ in: server_entry, out: 'index' })
87
+ return rebuildjs_server__build({
88
+ ...rebuildjs__config,
89
+ entryPoints,
90
+ plugins,
91
+ })
92
+ }
93
+ /**
94
+ * @param {rhonojs_plugin_config_T}[config]
95
+ * @returns {Plugin}
96
+ */
97
+ export function rhonojs_plugin_(config) {
98
+ return { name: 'rhonojs_plugin', setup: setup_() }
99
+ function setup_() {
100
+ const setup = build=>{
101
+ build.onEnd(async result=>{
102
+ if (result.errors.length) {
103
+ throw new Error(`Build errors: ${result.errors.length} errors`)
104
+ }
105
+ })
106
+ }
107
+ ref__bind(setup, rhonojs__link$_())
108
+ return setup
109
+ function rhonojs__link$_() {
110
+ return ns_id_be(
111
+ app_ctx,
112
+ 'app',
113
+ 'rhonojs__link$',
114
+ ctx=>
115
+ calling(memo_(()=>{
116
+ if (!rebuildjs__esbuild__done_(ctx)) return
117
+ nullish__none_([
118
+ build_id_(ctx),
119
+ server_entry__output__path_(ctx),
120
+ server_entry__output__link__path_(ctx),
121
+ ], (
122
+ build_id,
123
+ server_entry__output__path,
124
+ server_entry__output__link__path,
125
+ )=>{
126
+ run(async ()=>{
127
+ try {
128
+ await Promise.all([
129
+ file_exists__waitfor(async ()=>{
130
+ await cmd(
131
+ rm(server_entry__output__link__path, { force: true }))
132
+ await cmd(
133
+ link(server_entry__output__path, server_entry__output__link__path))
134
+ return true
135
+ }),
136
+ file_exists__waitfor(async ()=>{
137
+ await cmd(
138
+ rm(
139
+ server_entry__output__link__path + '.map',
140
+ { force: true }))
141
+ await cmd(
142
+ link(
143
+ server_entry__output__path + '.map',
144
+ server_entry__output__link__path + '.map'))
145
+ return true
146
+ })
147
+ ])
148
+ rhonojs__build_id__set(ctx, build_id)
149
+ if (config?.app__start ?? true) {
150
+ const _app = app_(ctx)
151
+ if (_app) {
152
+ // Previous Bun.serve server will be replaced
153
+ }
154
+ try {
155
+ await cmd(rhonojs__ready__wait(30_000))
156
+ await cmd(file_exists__waitfor(server_entry__output__link__path))
157
+ await cmd(file_exists__waitfor(async ()=>{
158
+ if (!await file_exists_(server_entry__output__link__path)) {
159
+ return false
160
+ }
161
+ let app = new Hono()
162
+ app.route('/',
163
+ await cmd(import(server_entry__output__link__path))
164
+ .then(mod=>mod.default()))
165
+ await cmd(app__start(app))
166
+ return true
167
+ }))
168
+ } catch (err) {
169
+ if (err instanceof Cancel) return
170
+ throw err
171
+ }
172
+ }
173
+ } catch (err) {
174
+ if (err instanceof Cancel) return
175
+ throw err
176
+ }
177
+ })
178
+ async function cmd(promise) {
179
+ if (cancel_()) promise__cancel__throw(promise)
180
+ if (!promise) return promise
181
+ ref__bind(promise, calling(memo_(rhonojs_cancel$=>{
182
+ if (cancel_()) {
183
+ promise__cancel(promise)
184
+ off(rhonojs_cancel$)
185
+ }
186
+ })))
187
+ const ret = await promise
188
+ if (cancel_()) promise__cancel__throw(promise)
189
+ return ret
190
+ }
191
+ function cancel_() {
192
+ return (
193
+ build_id_(ctx) !== build_id
194
+ || server_entry__output__path_(ctx) !== server_entry__output__path
195
+ || server_entry__output__link__path_(ctx) !== server_entry__output__link__path
196
+ )
197
+ }
198
+ })
199
+ })))
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,5 @@
1
+ import type { MiddlewareHandler } from 'hono'
2
+ export type compression_middleware_config_T = {
3
+ type?:'gzip'|'deflate'
4
+ }
5
+ export declare const compression_middleware_:(config?:compression_middleware_config_T)=>MiddlewareHandler
@@ -0,0 +1,11 @@
1
+ /// <reference types="./index.d.ts" />
2
+ import { compress } from 'hono/compress'
3
+ /**
4
+ * @param {compression_middleware_config_T}[config]
5
+ * @returns {import('hono').MiddlewareHandler}
6
+ */
7
+ export function compression_middleware_(config) {
8
+ return compress({
9
+ encoding: config?.type ?? 'gzip',
10
+ })
11
+ }
@@ -0,0 +1,34 @@
1
+ import type { static_export_config_T, static_export_result_T } from '../index.js'
2
+ export declare function cloudflare_export_(
3
+ config:cloudflare_export_config_T
4
+ ):Promise<cloudflare_export_result_T>
5
+ export declare function worker_entry__generate_(
6
+ dynamic_routes:route_handler_T[]
7
+ ):string
8
+ export declare function wrangler_toml__generate_(
9
+ overrides:Partial<wrangler_config_T>,
10
+ out_dir?:string
11
+ ):string
12
+ export type cloudflare_export_config_T =
13
+ & static_export_config_T
14
+ & {
15
+ dynamic_routes?:route_handler_T[]
16
+ worker_out?:string
17
+ wrangler?:Partial<wrangler_config_T>
18
+ }
19
+ export type cloudflare_export_result_T =
20
+ & static_export_result_T
21
+ & {
22
+ worker_entry_path?:string
23
+ wrangler_path:string
24
+ }
25
+ export type route_handler_T = {
26
+ pattern:string
27
+ handler:string
28
+ }
29
+ export type wrangler_config_T = {
30
+ name:string
31
+ compatibility_date:string
32
+ vars?:Record<string, string>
33
+ routes?:{ pattern:string, zone_name?:string }[]
34
+ }
@@ -0,0 +1,103 @@
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 { dirname, join, resolve } from 'node:path'
6
+ import { static_export_ } from '../index.js'
7
+ /**
8
+ * @param {cloudflare_export_config_T} config
9
+ * @returns {Promise<cloudflare_export_result_T>}
10
+ */
11
+ export async function cloudflare_export_(config) {
12
+ const {
13
+ dynamic_routes = [],
14
+ worker_out = 'dist/worker',
15
+ wrangler = {},
16
+ ...static_config
17
+ } = config
18
+ const static_result = await static_export_(static_config)
19
+ let worker_entry_path
20
+ if (dynamic_routes.length > 0) {
21
+ const entry_source = worker_entry__generate_(dynamic_routes)
22
+ const generated_path = join(worker_out, '_worker.src.js')
23
+ await mkdir(worker_out, { recursive: true })
24
+ await writeFile(generated_path, entry_source)
25
+ worker_entry_path = join(worker_out, '_worker.js')
26
+ await build({
27
+ entryPoints: [generated_path],
28
+ bundle: true,
29
+ format: 'esm',
30
+ target: 'es2022',
31
+ platform: 'browser',
32
+ outfile: worker_entry_path,
33
+ minify: true,
34
+ })
35
+ console.info(`[cloudflare_export] worker bundled: ${worker_entry_path}`)
36
+ }
37
+ const wrangler_config = wrangler_toml__generate_(wrangler, static_config.out_dir)
38
+ const wrangler_path = 'wrangler.toml'
39
+ await writeFile(wrangler_path, wrangler_config)
40
+ console.info(`[cloudflare_export] wrote ${wrangler_path}`)
41
+ return {
42
+ ...static_result,
43
+ worker_entry_path,
44
+ wrangler_path,
45
+ }
46
+ }
47
+ /**
48
+ * @param {route_handler_T[]} dynamic_routes
49
+ * @returns {string}
50
+ */
51
+ export function worker_entry__generate_(dynamic_routes) {
52
+ const imports = []
53
+ const route_checks = []
54
+ for (let i = 0; i < dynamic_routes.length; i++) {
55
+ const { pattern, handler } = dynamic_routes[i]
56
+ const handler_name = `handler_${i}`
57
+ imports.push(`import ${handler_name} from '${resolve(handler)}'`)
58
+ if (pattern.endsWith('/*')) {
59
+ const prefix = pattern.slice(0, -2)
60
+ route_checks.push(
61
+ `\tif (url.pathname.startsWith('${prefix}')) return ${handler_name}(request, env, ctx)`)
62
+ } else {
63
+ route_checks.push(
64
+ `\tif (url.pathname === '${pattern}') return ${handler_name}(request, env, ctx)`)
65
+ }
66
+ }
67
+ return `${imports.join('\n')}
68
+
69
+ export default {
70
+ async fetch(request, env, ctx) {
71
+ const url = new URL(request.url)
72
+ ${route_checks.join('\n')}
73
+ return env.ASSETS.fetch(request)
74
+ }
75
+ }
76
+ `
77
+ }
78
+ /**
79
+ * @param {Partial<wrangler_config_T>} overrides
80
+ * @param {string} [out_dir]
81
+ * @returns {string}
82
+ */
83
+ export function wrangler_toml__generate_(overrides, out_dir = 'dist/browser') {
84
+ const name = overrides.name || 'app'
85
+ const compatibility_date = overrides.compatibility_date || new Date().toISOString().slice(0, 10)
86
+ let toml = `name = "${name}"
87
+ compatibility_date = "${compatibility_date}"
88
+ pages_build_output_dir = "${out_dir}"
89
+ `
90
+ if (overrides.vars) {
91
+ toml += '\n[vars]\n'
92
+ for (const [key, val] of Object.entries(overrides.vars)) {
93
+ toml += `${key} = "${val}"\n`
94
+ }
95
+ }
96
+ if (overrides.routes) {
97
+ for (const route of overrides.routes) {
98
+ toml += `\n[[routes]]\npattern = "${route.pattern}"\n`
99
+ if (route.zone_name) toml += `zone_name = "${route.zone_name}"\n`
100
+ }
101
+ }
102
+ return toml
103
+ }
@@ -0,0 +1,28 @@
1
+ import type { Hono } from 'hono'
2
+ export declare function static_export_(
3
+ config:static_export_config_T
4
+ ):Promise<static_export_result_T>
5
+ export declare function static_export__file_path_(
6
+ route:string,
7
+ out_dir:string,
8
+ content_type?:string
9
+ ):string
10
+ export type static_export_config_T = {
11
+ routes?:string[]
12
+ site_url:string
13
+ out_dir?:string
14
+ base_url?:string
15
+ server_import?:string
16
+ app?:Hono
17
+ sitemap?:boolean
18
+ extra_routes?:string[]
19
+ url_rewrite?:boolean
20
+ incremental?:boolean
21
+ manifest?:boolean
22
+ clean?:boolean
23
+ on_export?:(route:string, file:string)=>void
24
+ }
25
+ export type static_export_result_T = {
26
+ exported:string[]
27
+ errors:string[]
28
+ }
@@ -0,0 +1,154 @@
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 { app_ctx, port_ } from 'rebuildjs/server'
6
+ import { app__start } from '../app/index.js'
7
+ /**
8
+ * @param {static_export_config_T} config
9
+ * @returns {Promise<static_export_result_T>}
10
+ */
11
+ export async function static_export_(config) {
12
+ const {
13
+ site_url,
14
+ out_dir = 'dist/browser',
15
+ server_import = './dist/server/index.js',
16
+ sitemap = true,
17
+ url_rewrite = true,
18
+ incremental = false,
19
+ manifest: use_manifest = incremental,
20
+ clean = false,
21
+ on_export,
22
+ } = config
23
+ let { routes = [], extra_routes = [] } = config
24
+ const site_origin = site_url.replace(/\/$/, '')
25
+ const exported = []
26
+ const errors = []
27
+ let app = config.app
28
+ let we_started = false
29
+ let base_url = config.base_url
30
+ let server
31
+ try {
32
+ if (base_url) {
33
+ base_url = base_url.replace(/\/$/, '')
34
+ console.info(`[static_export] using external server: ${base_url}`)
35
+ } else if (!app) {
36
+ const mod = await import(server_import)
37
+ app = typeof mod.default === 'function' ? await mod.default() : mod.default
38
+ await app__start(app)
39
+ we_started = true
40
+ base_url = `http://localhost:${port_(app_ctx)}`
41
+ console.info(`[static_export] server running on ${base_url}`)
42
+ } else {
43
+ base_url = `http://localhost:${port_(app_ctx)}`
44
+ console.info(`[static_export] using provided app on ${base_url}`)
45
+ }
46
+ console.info(`[static_export] site_url: ${site_origin}`)
47
+ if (clean) {
48
+ await rm(out_dir, { recursive: true, force: true })
49
+ }
50
+ if (sitemap) {
51
+ try {
52
+ const res = await fetch(`${base_url}/sitemap.xml`)
53
+ if (res.ok) {
54
+ const xml = await res.text()
55
+ for (const m of xml.matchAll(/<loc>https?:\/\/[^<]+<\/loc>/g)) {
56
+ const url = m[0].replace(/<\/?loc>/g, '')
57
+ const path = new URL(url).pathname
58
+ const route = path === '' ? '/' : path
59
+ if (!routes.includes(route)) {
60
+ routes.push(route)
61
+ }
62
+ }
63
+ console.info(`[static_export] discovered ${routes.length} routes from sitemap`)
64
+ }
65
+ } catch {
66
+ console.warn('[static_export] sitemap fetch failed, using explicit routes only')
67
+ }
68
+ }
69
+ const manifest_path = join(out_dir, '.export-manifest.json')
70
+ let prev_manifest = []
71
+ if (use_manifest) {
72
+ try {
73
+ prev_manifest = JSON.parse(await readFile(manifest_path, 'utf-8'))
74
+ } catch {
75
+ // No previous manifest
76
+ }
77
+ }
78
+ const all_routes = [...routes, ...extra_routes]
79
+ for (const route of all_routes) {
80
+ const url = base_url + route
81
+ try {
82
+ const res = await fetch(url)
83
+ if (!res.ok) {
84
+ console.error(`[static_export] ${route} -> ${res.status} ${res.statusText}`)
85
+ errors.push(route)
86
+ continue
87
+ }
88
+ const content_type = res.headers.get('content-type') ?? ''
89
+ let body = await res.text()
90
+ if (url_rewrite) {
91
+ body = body.replaceAll(base_url, site_origin)
92
+ }
93
+ const file_path = static_export__file_path_(route, out_dir, content_type)
94
+ if (incremental) {
95
+ try {
96
+ const existing = await readFile(file_path, 'utf-8')
97
+ if (existing === body) {
98
+ exported.push(file_path)
99
+ continue
100
+ }
101
+ } catch {
102
+ // File doesn't exist yet
103
+ }
104
+ }
105
+ await mkdir(dirname(file_path), { recursive: true })
106
+ await writeFile(file_path, body)
107
+ exported.push(file_path)
108
+ on_export?.(route, file_path)
109
+ console.info(`[static_export] ${route} -> ${file_path} (${body.length} bytes)`)
110
+ } catch (err) {
111
+ console.error(`[static_export] ${route} -> ${err.message}`)
112
+ errors.push(route)
113
+ }
114
+ }
115
+ if (use_manifest) {
116
+ await mkdir(dirname(manifest_path), { recursive: true })
117
+ await writeFile(manifest_path, JSON.stringify(exported, null, '\t'))
118
+ const stale = prev_manifest.filter(f=>!exported.includes(f))
119
+ for (const file of stale) {
120
+ try {
121
+ await unlink(file)
122
+ console.info(`[static_export] deleted stale: ${file}`)
123
+ } catch {
124
+ // Already gone
125
+ }
126
+ }
127
+ }
128
+ if (errors.length > 0) {
129
+ console.warn(`[static_export] completed with ${errors.length} error(s)`)
130
+ } else {
131
+ console.info(`[static_export] exported ${exported.length} files`)
132
+ }
133
+ } finally {
134
+ // Bun.serve cleanup is handled by process exit
135
+ }
136
+ return { exported, errors }
137
+ }
138
+ /**
139
+ * @param {string} route
140
+ * @param {string} out_dir
141
+ * @param {string} [content_type]
142
+ * @returns {string}
143
+ */
144
+ export function static_export__file_path_(route, out_dir, content_type) {
145
+ const ext = extname(route)
146
+ const is_html = content_type ? content_type.includes('text/html') : !ext || ext === '.html'
147
+ if (ext && !is_html) {
148
+ return join(out_dir, route)
149
+ }
150
+ if (route === '/') {
151
+ return join(out_dir, 'index.html')
152
+ }
153
+ return join(out_dir, route, 'index.html')
154
+ }
@@ -0,0 +1,9 @@
1
+ import type { ctx__be_T, ctx__get_T, ctx__set_T, sig_T } from 'ctx-core/rmemo'
2
+ import type { Context } from 'hono'
3
+ export declare const hono_context$_:ctx__be_T<sig_T<Context|undefined>, 'request'>
4
+ export declare const hono_context_:ctx__get_T<Context|undefined, 'request'>
5
+ export declare const hono_context__set:ctx__set_T<Context|undefined, 'request'>
6
+ export declare const request$_:ctx__be_T<sig_T<Request|undefined>, 'request'>
7
+ export declare const request_:ctx__get_T<Request|undefined, 'request'>
8
+ export declare const request_url$_:ctx__be_T<sig_T<URL|undefined>, 'request'>
9
+ export declare const request_url_:ctx__get_T<URL|undefined, 'request'>
@@ -0,0 +1,26 @@
1
+ import { ns_id_be_memo_pair_, ns_id_be_sig_triple_, nullish__none_ } from 'ctx-core/rmemo'
2
+ export const [
3
+ hono_context$_,
4
+ hono_context_,
5
+ hono_context__set,
6
+ ] = ns_id_be_sig_triple_(
7
+ 'request',
8
+ 'hono_context',
9
+ ()=>undefined)
10
+ export const [
11
+ request$_,
12
+ request_,
13
+ ] = ns_id_be_memo_pair_(
14
+ 'request',
15
+ 'request',
16
+ ctx=>
17
+ hono_context_(ctx)?.req?.raw)
18
+ export const [
19
+ request_url$_,
20
+ request_url_,
21
+ ] = ns_id_be_memo_pair_(
22
+ 'request',
23
+ 'request_url',
24
+ ctx=>
25
+ nullish__none_([request_(ctx)],
26
+ request=>new URL(request.url)))
@@ -0,0 +1,7 @@
1
+ export * from 'rebuildjs/server'
2
+ export * from './app/index.js'
3
+ export * from './build/index.js'
4
+ export * from './compression/index.js'
5
+ export * from './hono/index.js'
6
+ export * from './route/index.js'
7
+ export * from './static/index.js'
@@ -0,0 +1,7 @@
1
+ export * from 'rebuildjs/server'
2
+ export * from './app/index.js'
3
+ export * from './build/index.js'
4
+ export * from './compression/index.js'
5
+ export * from './hono/index.js'
6
+ export * from './route/index.js'
7
+ export * from './static/index.js'
@@ -0,0 +1,16 @@
1
+ /// <reference lib="dom" />
2
+ import type { Context } from 'hono'
3
+ import type { middleware_ctx_T, request_ctx_T } from 'rebuildjs/server'
4
+ export declare function html_route_(
5
+ middleware_ctx:middleware_ctx_T,
6
+ page_:($p:{ ctx:request_ctx_T })=>({ toString():string }|ReadableStream<string>),
7
+ response_init?:ResponseInit
8
+ ):(c:Context)=>Response
9
+ export declare function request_ctx__ensure(
10
+ middleware_ctx:middleware_ctx_T,
11
+ c:Context,
12
+ ):request_ctx_T
13
+ export declare function html_response__new(
14
+ html_OR_stream:string|ReadableStream,
15
+ response_init?:ResponseInit
16
+ ):Response
@@ -0,0 +1,63 @@
1
+ /// <reference types="./index.d.ts" />
2
+ import { request_ctx__new } from 'rebuildjs/server'
3
+ import { hono_context__set } from '../hono/index.js'
4
+ /**
5
+ * @param {middleware_ctx_T}middleware_ctx
6
+ * @param {($p:{ ctx:request_ctx_T })=>(string|ReadableStream<string|Uint8Array>)}page_
7
+ * @param {ResponseInit}[response_init]
8
+ * @returns {(c:Context)=>Response}
9
+ */
10
+ export function html_route_(
11
+ middleware_ctx,
12
+ page_,
13
+ response_init
14
+ ) {
15
+ return c=>{
16
+ const request_ctx = request_ctx__ensure(middleware_ctx, c)
17
+ return html_response__new(
18
+ page_({ ctx: request_ctx }),
19
+ response_init)
20
+ }
21
+ }
22
+ /**
23
+ * @param {string|ReadableStream}html_OR_stream
24
+ * @param {ResponseInit}[response_init]
25
+ * @returns {Response}
26
+ */
27
+ export function html_response__new(
28
+ html_OR_stream,
29
+ response_init
30
+ ) {
31
+ const headers = new Headers(response_init?.headers)
32
+ headers.set('Content-Type', 'text/html;charset=UTF-8')
33
+ return new Response(
34
+ html_OR_stream.pipeTo
35
+ ? html_OR_stream.pipeThrough(new TextEncoderStream())
36
+ : new ReadableStream({
37
+ start(controller) {
38
+ controller.enqueue('' + html_OR_stream)
39
+ controller.close()
40
+ }
41
+ }),
42
+ {
43
+ ...(response_init ?? {}),
44
+ headers
45
+ }
46
+ )
47
+ }
48
+ /**
49
+ * @param {middleware_ctx_T}middleware_ctx
50
+ * @param {Context}c
51
+ */
52
+ export function request_ctx__ensure(
53
+ middleware_ctx,
54
+ c,
55
+ ) {
56
+ let request_ctx = c.get('request_ctx')
57
+ if (!request_ctx) {
58
+ request_ctx = request_ctx__new(middleware_ctx)
59
+ c.set('request_ctx', request_ctx)
60
+ }
61
+ hono_context__set(request_ctx, c)
62
+ return request_ctx
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { Hono } from 'hono'
2
+ export declare function static_middleware_(
3
+ config?:static_middleware__config_T
4
+ ):Promise<Hono>
5
+ export type static_middleware__config_T = {
6
+ headers_?:(url_path:string, content_type:string, path:string)=>Record<string, string>
7
+ }
@@ -0,0 +1,34 @@
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 { Hono } from 'hono'
6
+ import { extname, join } from 'node:path'
7
+ import { app_ctx, browser_path_ } from 'rebuildjs/server'
8
+ /**
9
+ * @param {static_middleware__config_T}[config]
10
+ * @returns {Promise<Hono>}
11
+ */
12
+ export async function static_middleware_(config) {
13
+ const app = new Hono()
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
+ app.get(url_path, c=>{
24
+ const file_ref = file(path)
25
+ return new Response(file_ref.size ? file_ref.stream() : '', {
26
+ headers: {
27
+ 'Content-Type': content_type,
28
+ ...headers,
29
+ }
30
+ })
31
+ })
32
+ }
33
+ return app
34
+ }
@@ -0,0 +1 @@
1
+ export * from 'rebuildjs/types'