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 +10 -2
- package/server/build/index.d.ts +5 -0
- package/server/build/index.js +70 -3
- package/server/export/cloudflare/index.d.ts +37 -0
- package/server/export/cloudflare/index.js +108 -0
- package/server/export/index.d.ts +29 -0
- package/server/export/index.js +168 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +2 -0
- package/server/route/index.d.ts +12 -0
- package/server/route/index.js +52 -0
- package/server/static/index.d.ts +11 -0
- package/server/static/index.js +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rebuildjs",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/server/build/index.d.ts
CHANGED
|
@@ -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'>
|
package/server/build/index.js
CHANGED
|
@@ -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
package/server/index.js
CHANGED
|
@@ -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
|
+
}
|