@rip-lang/server 1.3.67 → 1.3.69

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.
Files changed (2) hide show
  1. package/middleware.rip +54 -33
  2. package/package.json +2 -2
package/middleware.rip CHANGED
@@ -473,20 +473,28 @@ pre{margin:0;white-space:pre-wrap;word-break:break-word}
473
473
  # Hot-reload SSE is handled by rip-server; this middleware registers watch dirs.
474
474
  #
475
475
  # Options:
476
- # dir: string — app directory on disk (default: '.')
477
- # routes: string — page components directory, relative to dir (default: 'routes')
478
- # components: string[] shared component directories, relative to dir (default: ['components'])
479
- # app: string — URL mount point (default: '')
480
- # title: string document title
481
- # state: object initial app state passed via bundle
482
- # watch: boolean enable hot-reload (registers watch dirs with rip-server)
476
+ # dir: string — app directory on disk (default: '.')
477
+ # routes: string — page components directory, relative to dir (default: 'routes')
478
+ # bundle: string[]|object — directories for bundle endpoint(s) (default: ['components'])
479
+ # Array: single bundle at {prefix}/bundle (legacy)
480
+ # Object: named bundles { ui: [...], app: [...] } → {prefix}/ui, {prefix}/app
481
+ # app: string URL mount point (default: '')
482
+ # title: string document title
483
+ # state: object — initial app state passed via bundle
484
+ # watch: boolean — enable hot-reload (registers watch dirs with rip-server)
483
485
  #
484
486
 
485
487
  export serve = (opts = {}) ->
486
488
  prefix = opts.app or ''
487
489
  appDir = opts.dir or '.'
488
490
  routesDir = "#{appDir}/#{opts.routes or 'routes'}"
489
- componentDirs = (opts.components or ['components']).map (d) -> "#{appDir}/#{d}"
491
+ rawBundle = opts.bundle or opts.components or ['components']
492
+ bundles = new Map()
493
+ if Array.isArray(rawBundle)
494
+ bundles.set 'bundle', rawBundle.map (d) -> "#{appDir}/#{d}"
495
+ else if typeof rawBundle is 'object'
496
+ for name, dirs of rawBundle
497
+ bundles.set name, dirs.map (d) -> "#{appDir}/#{d}"
490
498
  enableWatch = if opts.watch? then opts.watch else process.env.SOCKET_PREFIX?
491
499
  appState = opts.state or null
492
500
  appTitle = opts.title or null
@@ -524,43 +532,56 @@ export serve = (opts = {}) ->
524
532
  return unless path.startsWith(appDirResolved)
525
533
  @send path, 'text/x-rip; charset=UTF-8' if existsSync(path)
526
534
 
527
- # Route: {prefix}/bundleall components + app data as JSON (cached + Brotli)
528
- bundleCache = { json: null, br: null, etag: null }
535
+ # Routes: {prefix}/{name}per-bundle JSON endpoints (cached + Brotli)
536
+ # Built once per worker on first request, then served from memory with ETag/304.
537
+ bundleCaches = new Map()
529
538
 
530
- buildBundle = ->
539
+ buildNamedBundle = (name, dirs, includeRoutes = false) ->
531
540
  { brotliCompressSync } = require 'zlib'
532
541
  glob = new Bun.Glob("**/*.rip")
533
542
  components = {}
534
- for path in Array.from(glob.scanSync(routesDir)).sort()
535
- components["components/#{path}"] = Bun.file("#{routesDir}/#{path}").text!
536
- for dir in componentDirs
543
+ if includeRoutes
544
+ for path in Array.from(glob.scanSync(routesDir)).sort()
545
+ components["components/#{path}"] = Bun.file("#{routesDir}/#{path}").text!
546
+ for dir in dirs
537
547
  for path in Array.from(glob.scanSync(dir)).sort()
538
548
  continue if path is 'index.rip' or path.endsWith('/index.rip')
539
549
  key = "components/_lib/#{path}"
540
550
  components[key] = Bun.file("#{dir}/#{path}").text! unless components[key]
541
- data = {}
542
- data.title = appTitle if appTitle
543
- data.watch = enableWatch
544
- if appState
545
- data[k] = v for k, v of appState
546
- bundleCache.json = JSON.stringify({ components, data })
547
- bundleCache.br = brotliCompressSync(bundleCache.json)
548
- bundleCache.etag = "W/\"#{Bun.hash(bundleCache.json).toString(36)}\""
549
-
550
- get "#{prefix}/bundle", (c) ->
551
- buildBundle! unless bundleCache.json
552
- if c.req.header('if-none-match') is bundleCache.etag
553
- return new Response(null, { status: 304, headers: { 'ETag': bundleCache.etag, 'Cache-Control': 'no-cache' } })
554
- useBr = (c.req.header('accept-encoding') or '').includes('br')
555
- headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'ETag': bundleCache.etag }
556
- headers['Content-Encoding'] = 'br' if useBr
557
- new Response (if useBr then bundleCache.br else bundleCache.json), { headers }
551
+ bundle = { components }
552
+ if includeRoutes
553
+ data = {}
554
+ data.title = appTitle if appTitle
555
+ data.watch = enableWatch
556
+ if appState
557
+ data[k] = v for k, v of appState
558
+ bundle.data = data
559
+ cache = { json: null, br: null, etag: null }
560
+ cache.json = JSON.stringify(bundle)
561
+ cache.br = brotliCompressSync(cache.json)
562
+ cache.etag = "W/\"#{Bun.hash(cache.json).toString(36)}\""
563
+ bundleCaches.set name, cache
564
+
565
+ for [name, dirs] as bundles
566
+ get "#{prefix}/#{name}", (c) ->
567
+ cache = bundleCaches.get(name)
568
+ buildNamedBundle(name, dirs, (name is 'app') or (bundles.size is 1)) unless cache
569
+ cache = bundleCaches.get(name)
570
+ if c.req.header('if-none-match') is cache.etag
571
+ return new Response(null, { status: 304, headers: { 'ETag': cache.etag, 'Cache-Control': 'no-cache' } })
572
+ useBr = (c.req.header('accept-encoding') or '').includes('br')
573
+ headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache', 'ETag': cache.etag }
574
+ headers['Content-Encoding'] = 'br' if useBr
575
+ new Response (if useBr then cache.br else cache.json), { headers }
558
576
 
559
577
  # Register watch directories with rip-server via control socket
560
578
  if enableWatch and process.env.SOCKET_PREFIX
561
579
  ctl = "/tmp/#{process.env.SOCKET_PREFIX}.ctl.sock"
562
- dirs = [appDir].filter existsSync
563
- body = JSON.stringify({ op: 'watch', prefix, dirs })
580
+ watchDirs = [appDir]
581
+ for [_, bdirs] as bundles
582
+ watchDirs.push(...bdirs)
583
+ watchDirs = watchDirs.filter existsSync
584
+ body = JSON.stringify({ op: 'watch', prefix, dirs: watchDirs })
564
585
  fetch('http://localhost/watch', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch (e) ->
565
586
  console.warn "[Rip] Watch registration failed: #{e.message}"
566
587
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.3.67",
3
+ "version": "1.3.69",
4
4
  "description": "Pure Rip web framework and application server",
5
5
  "type": "module",
6
6
  "main": "api.rip",
@@ -45,7 +45,7 @@
45
45
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
46
46
  "license": "MIT",
47
47
  "dependencies": {
48
- "rip-lang": ">=3.13.83"
48
+ "rip-lang": ">=3.13.85"
49
49
  },
50
50
  "files": [
51
51
  "api.rip",