rip-lang 3.13.136 → 3.14.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/src/ui.rip CHANGED
@@ -30,32 +30,35 @@ export { setContext, getContext, hasContext }
30
30
  #
31
31
  # Wraps a plain object in a Proxy that lazily creates fine-grained signals
32
32
  # for each property. Nested objects are wrapped recursively. Supports path-
33
- # based access via get/set methods (e.g., stash.get('users[0].name')).
33
+ # based bracket access (e.g., app['data/theme/title']) and 7 reserved methods
34
+ # for atomic/structural operations: inc, dec, flip, join, keys, has, del.
34
35
  # ==============================================================================
35
36
 
36
- STASH = Symbol('stash')
37
- SIGNALS = Symbol('signals')
38
- RAW = Symbol('raw')
39
- PERSISTED = Symbol('persisted')
40
37
  PROXIES = new WeakMap()
38
+ METHODS = new WeakMap()
41
39
 
42
40
  _keysVersion = 0
43
41
  _writeVersion = __state(0)
42
+ _depth = 0
43
+
44
+ isPathKey = (prop) -> typeof prop is 'string' and (prop.indexOf('.') isnt -1 or prop.indexOf('/') isnt -1 or prop.indexOf('[') isnt -1)
45
+
46
+ isNum = (val) -> /^-?\d+$/.test val
44
47
 
45
48
  getSignal = (target, prop) ->
46
- unless target[SIGNALS]
47
- Object.defineProperty target, SIGNALS, { value: new Map(), enumerable: false }
48
- sig = target[SIGNALS].get(prop)
49
+ unless target[:signals]
50
+ Object.defineProperty target, :signals, { value: new Map(), enumerable: false }
51
+ sig = target[:signals].get(prop)
49
52
  unless sig
50
53
  sig = __state(target[prop])
51
- target[SIGNALS].set(prop, sig)
54
+ target[:signals].set(prop, sig)
52
55
  sig
53
56
 
54
- keysSignal = (target) -> getSignal(target, Symbol.for('keys'))
57
+ keysSignal = (target) -> getSignal(target, :keys)
55
58
 
56
59
  wrapDeep = (value) ->
57
60
  return value unless value? and typeof value is 'object'
58
- return value if value[STASH]
61
+ return value if value[:stash]
59
62
  return value if value instanceof Date or value instanceof RegExp or value instanceof Map or value instanceof Set or value instanceof Promise
60
63
  existing = PROXIES.get(value)
61
64
  return existing if existing
@@ -65,17 +68,19 @@ makeProxy = (target) ->
65
68
  proxy = null
66
69
  handler =
67
70
  get: (target, prop) ->
68
- return true if prop is STASH
69
- return target if prop is RAW
71
+ return true if prop is :stash
72
+ return target if prop is :raw
70
73
  return Reflect.get(target, prop) if typeof prop is 'symbol'
71
74
 
72
75
  if prop is 'length' and Array.isArray(target)
73
76
  keysSignal(target).value
74
77
  return target.length
75
78
 
76
- # Stash API methods
77
- return ((path) -> stashGet(proxy, path)) if prop is 'get'
78
- return ((path, val) -> stashSet(proxy, path, val)) if prop is 'set'
79
+ if not _depth and isPathKey(prop)
80
+ return stashGet(proxy, prop)
81
+
82
+ fn = stashMethodFn(proxy, prop)
83
+ return fn if fn
79
84
 
80
85
  sig = getSignal(target, prop)
81
86
  val = sig.value
@@ -84,13 +89,17 @@ makeProxy = (target) ->
84
89
  val
85
90
 
86
91
  set: (target, prop, value) ->
92
+ if not _depth and isPathKey(prop)
93
+ stashSet(proxy, prop, value)
94
+ return true
95
+
87
96
  old = target[prop]
88
- r = if value?[RAW] then value[RAW] else value
97
+ r = if value?[:raw] then value[:raw] else value
89
98
  return true if r is old
90
99
  target[prop] = r
91
100
 
92
- if target[SIGNALS]?.has(prop)
93
- target[SIGNALS].get(prop).value = r
101
+ if target[:signals]?.has(prop)
102
+ target[:signals].get(prop).value = r
94
103
  if old is undefined and r isnt undefined
95
104
  keysSignal(target).value = ++_keysVersion
96
105
  _writeVersion.value++
@@ -99,9 +108,10 @@ makeProxy = (target) ->
99
108
 
100
109
  deleteProperty: (target, prop) ->
101
110
  delete target[prop]
102
- sig = target[SIGNALS]?.get(prop)
111
+ sig = target[:signals]?.get(prop)
103
112
  sig?.value = undefined
104
113
  keysSignal(target).value = ++_keysVersion
114
+ _writeVersion.value++
105
115
  true
106
116
 
107
117
  ownKeys: (target) ->
@@ -133,35 +143,126 @@ walk = (path) ->
133
143
  i += 2
134
144
  result
135
145
 
146
+ resolveIndex = (seg, obj) ->
147
+ if typeof seg is 'number' and seg < 0
148
+ t = raw(obj)
149
+ return t.length + seg if Array.isArray(t)
150
+ seg
151
+
136
152
  stashGet = (proxy, path) ->
137
153
  segs = walk(path)
138
154
  obj = proxy
139
- for seg in segs
140
- return undefined unless obj?
141
- obj = obj[seg]
142
- obj
155
+ _depth++
156
+ try
157
+ for seg in segs
158
+ return undefined unless obj?
159
+ obj = obj[resolveIndex(seg, obj)]
160
+ obj
161
+ finally
162
+ _depth--
143
163
 
144
164
  stashSet = (proxy, path, value) ->
145
165
  segs = walk(path)
146
166
  obj = proxy
147
- for seg, i in segs
148
- if i is segs.length - 1
149
- obj[seg] = value
150
- else
151
- obj[seg] = {} unless obj[seg]?
152
- obj = obj[seg]
153
- value
167
+ _depth++
168
+ try
169
+ for seg, i in segs
170
+ key = resolveIndex(seg, obj)
171
+ if i is segs.length - 1
172
+ obj[key] = value
173
+ else
174
+ unless obj[key]?
175
+ nextSeg = segs[i + 1]
176
+ obj[key] = if typeof nextSeg is 'number' or isNum(nextSeg) then [] else {}
177
+ obj = obj[key]
178
+ value
179
+ finally
180
+ _depth--
181
+
182
+ # Stash methods — reserved names for atomic/structural operations
183
+ STASH_METHOD_NAMES = { inc: true, dec: true, flip: true, join: true, keys: true, has: true, del: true }
184
+
185
+ stashMethodFn = (proxy, prop) ->
186
+ return undefined unless STASH_METHOD_NAMES[prop]
187
+ cache = METHODS.get(proxy)
188
+ unless cache
189
+ cache = {}
190
+ METHODS.set(proxy, cache)
191
+ return cache[prop] if cache[prop]
192
+ fn = switch prop
193
+ when 'inc' then (path, step = 1) -> cur = stashGet(proxy, path) ?? 0; stashSet(proxy, path, cur + step)
194
+ when 'dec' then (path, step = 1) -> cur = stashGet(proxy, path) ?? 0; stashSet(proxy, path, cur - step)
195
+ when 'flip' then (path) -> cur = stashGet(proxy, path) ?? false; stashSet(proxy, path, not cur)
196
+ when 'join' then (path, obj) ->
197
+ __batch ->
198
+ target = stashGet(proxy, path)
199
+ unless target? and typeof target is 'object'
200
+ stashSet(proxy, path, {})
201
+ target = stashGet(proxy, path)
202
+ _depth++
203
+ try
204
+ target[k] = v for k, v of obj
205
+ finally
206
+ _depth--
207
+ when 'keys' then (path) ->
208
+ _depth++
209
+ try
210
+ segs = walk(path)
211
+ obj = proxy
212
+ for seg in segs
213
+ return [] unless obj?
214
+ obj = obj[resolveIndex(seg, obj)]
215
+ return [] unless obj? and typeof obj is 'object'
216
+ t = raw(obj)
217
+ keysSignal(t).value
218
+ Object.keys(t)
219
+ finally
220
+ _depth--
221
+ when 'has' then (path) ->
222
+ _depth++
223
+ try
224
+ segs = walk(path)
225
+ return false unless segs.length > 0
226
+ obj = proxy
227
+ for seg, i in segs
228
+ key = resolveIndex(seg, obj)
229
+ if i is segs.length - 1
230
+ t = raw(obj)
231
+ keysSignal(t).value
232
+ return Object.prototype.hasOwnProperty.call(t, key)
233
+ return false unless obj?
234
+ obj = obj[key]
235
+ false
236
+ finally
237
+ _depth--
238
+ when 'del' then (path) ->
239
+ _depth++
240
+ try
241
+ segs = walk(path)
242
+ return unless segs.length > 0
243
+ obj = proxy
244
+ for seg, i in segs
245
+ key = resolveIndex(seg, obj)
246
+ if i is segs.length - 1
247
+ delete obj[key]
248
+ return
249
+ return unless obj?
250
+ obj = obj[key]
251
+ finally
252
+ _depth--
253
+ cache[prop] = fn
254
+ fn
154
255
 
155
256
  export stash = (data = {}) -> makeProxy(data)
156
257
 
157
- export raw = (proxy) -> if proxy?[RAW] then proxy[RAW] else proxy
258
+ export raw = (proxy) -> if proxy?[:raw] then proxy[:raw] else proxy
158
259
 
159
- export isStash = (obj) -> obj?[STASH] is true
260
+ export isStash = (obj) -> obj?[:stash] is true
160
261
 
161
262
  export persistStash = (app, opts = {}) ->
162
263
  target = raw(app) or app
163
- return if target[PERSISTED]
164
- target[PERSISTED] = true
264
+ return if target[:persisted]
265
+ target[:persisted] = true
165
266
  storage = if opts.local then localStorage else sessionStorage
166
267
  storageKey = opts.key or '__rip_app'
167
268
  try
@@ -626,18 +727,77 @@ buildComponentMap = (components, root = 'components') ->
626
727
  map[name] = path
627
728
  map
628
729
 
730
+ # Resolve a .rip import specifier to a path in the component store
731
+ resolveStorePath = (specifier, currentPath, components) ->
732
+ clean = specifier.replace(/^(\.\.\/|\.\/)+/, '')
733
+ basename = clean.split('/').pop()
734
+ # Direct resolution relative to current file in the store
735
+ if currentPath
736
+ parts = currentPath.split('/')
737
+ parts.pop()
738
+ for seg in specifier.split('/')
739
+ if seg is '..'
740
+ parts.pop()
741
+ else unless seg is '.'
742
+ parts.push seg
743
+ candidate = parts.join('/')
744
+ return candidate if components.exists(candidate)
745
+ # Try common store prefixes
746
+ return "components/_lib/#{clean}" if components.exists("components/_lib/#{clean}")
747
+ return "components/#{clean}" if components.exists("components/#{clean}")
748
+ # Basename search as last resort
749
+ for p in components.listAll('components')
750
+ return p if p.endsWith("/#{basename}")
751
+ null
752
+
629
753
  compileAndImport = (source, compile, components = null, path = null, resolver = null) ->
630
754
  # Check compilation cache
631
755
  if components and path
632
756
  cached = components.getCompiled(path)
633
757
  return cached if cached
634
758
 
759
+ # Mark as in-progress to prevent circular dependency loops
760
+ if resolver and path
761
+ resolver.compiling ?= {}
762
+ resolver.compiling[path] = true
763
+
635
764
  js = compile(source)
636
765
 
637
- # Resolve component dependencies — scan for PascalCase references in compiled JS
638
766
  if resolver
767
+ importedNames = new Set()
768
+
769
+ # Step 1: Rewrite .rip imports to blob URLs
770
+ if components
771
+ ripImportRe = /^(\s*import\s+(?:\{([^}]+)\}\s+from\s+|.*?\s+from\s+)?['"])([^'"]*\.rip)(['"];?\s*)$/gm
772
+ matches = Array.from(js.matchAll(ripImportRe))
773
+ # Walk matches in reverse so string indices stay valid after each splice
774
+ for m in matches by -1
775
+ [full, pre, namedImports, specifier, post] = m
776
+ storePath = resolveStorePath(specifier, path, components)
777
+ continue if storePath is path
778
+ unless storePath
779
+ msg = "[Rip] Could not resolve import: #{specifier}"
780
+ msg += " (from #{path})" if path
781
+ console.warn msg
782
+ continue
783
+ # Guard against circular imports — skip if already being compiled
784
+ unless resolver.blobUrls?[storePath]
785
+ continue if resolver.compiling?[storePath]
786
+ depSource = components.read(storePath)
787
+ if depSource
788
+ compileAndImport! depSource, compile, components, storePath, resolver
789
+ blobUrl = resolver.blobUrls?[storePath]
790
+ if blobUrl
791
+ replacement = "#{pre}#{blobUrl}#{post}"
792
+ js = js.slice(0, m.index) + replacement + js.slice(m.index + full.length)
793
+ if namedImports
794
+ for n in namedImports.split(',')
795
+ importedNames.add(n.trim().split(/\s+as\s+/).pop().trim())
796
+
797
+ # Step 2: Implicit component resolution — for template-used components not explicitly imported
639
798
  needed = {}
640
799
  for name, depPath of resolver.map
800
+ continue if importedNames.has(name)
641
801
  if depPath isnt path and js.includes("new #{name}(")
642
802
  unless resolver.classes[name]
643
803
  depSource = components.read(depPath)
@@ -656,6 +816,13 @@ compileAndImport = (source, compile, components = null, path = null, resolver =
656
816
  header = if path then "// #{path}\n" else ''
657
817
  blob = new Blob([header + js], { type: 'application/javascript' })
658
818
  url = URL.createObjectURL(blob)
819
+
820
+ # Cache blob URL so other files can rewrite imports to point here
821
+ if resolver and path
822
+ resolver.blobUrls ?= {}
823
+ resolver.blobUrls[path] = url
824
+ delete resolver.compiling[path] if resolver.compiling
825
+
659
826
  mod = await import(url)
660
827
 
661
828
  # Register any components from this module
@@ -680,9 +847,6 @@ export createRenderer = (opts = {}) ->
680
847
  container.id = 'app'
681
848
  document.body.appendChild container
682
849
 
683
- # Fade in after first mount (prevents layout-before-content flicker)
684
- container.style.opacity = '0'
685
-
686
850
  currentComponent = null
687
851
  currentRoute = null
688
852
  currentParams = null
@@ -816,15 +980,9 @@ export createRenderer = (opts = {}) ->
816
980
  instance.load!(params, query) if instance.load
817
981
  oldTarget?.remove()
818
982
  router.navigating = false
819
- if container.style.opacity is '0'
820
- document.fonts.ready.then ->
821
- requestAnimationFrame ->
822
- container.style.transition = 'opacity 150ms ease-in'
823
- container.style.opacity = '1'
824
983
 
825
984
  catch err
826
985
  router.navigating = false
827
- container.style.opacity = '1'
828
986
  console.error "Renderer: error mounting #{route.file}:", err
829
987
  onError({ status: 500, message: err.message, error: err }) if onError
830
988