fusee-framework 2.7.0 → 2.7.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.
Files changed (61) hide show
  1. package/README.md +56 -2
  2. package/bin/create-fusee-linux +0 -0
  3. package/bin/create-fusee-mac-arm64 +0 -0
  4. package/bin/create-fusee-mac-intel +0 -0
  5. package/bin/create-fusee-win.exe +0 -0
  6. package/framework/auto-import-preset.js +20 -12
  7. package/framework/auto-imports.d.ts +68 -39
  8. package/framework/core/compiler.js +310 -310
  9. package/framework/core/component-core.js +445 -0
  10. package/framework/core/component.js +11 -440
  11. package/framework/core/composable.js +29 -29
  12. package/framework/core/h.js +588 -557
  13. package/framework/core/jsx-helpers.js +11 -2
  14. package/framework/core/jsx-runtime.js +73 -73
  15. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm +0 -0
  16. package/framework/core/store.js +333 -333
  17. package/framework/globals.js +42 -0
  18. package/framework/index.js +22 -55
  19. package/framework/router/file-router.js +184 -184
  20. package/framework/server/actions.js +183 -183
  21. package/framework/server/generate-manifest.js +3 -3
  22. package/framework/types/component.d.ts +97 -86
  23. package/framework/types/index.d.ts +118 -117
  24. package/package.json +83 -80
  25. package/framework/core/rust-compiler/Cargo.toml +0 -31
  26. package/framework/core/rust-compiler/README.md +0 -69
  27. package/framework/core/rust-compiler/pkg/README.md +0 -69
  28. package/framework/core/rust-compiler/pkg/fusee_compiler.d.ts +0 -6
  29. package/framework/core/rust-compiler/pkg/fusee_compiler.js +0 -9
  30. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.js +0 -137
  31. package/framework/core/rust-compiler/pkg/fusee_compiler_bg.wasm.d.ts +0 -11
  32. package/framework/core/rust-compiler/pkg/package.json +0 -18
  33. package/framework/core/rust-compiler/src/ast.rs +0 -274
  34. package/framework/core/rust-compiler/src/errors.rs +0 -204
  35. package/framework/core/rust-compiler/src/generator.rs +0 -583
  36. package/framework/core/rust-compiler/src/lexer.rs +0 -414
  37. package/framework/core/rust-compiler/src/lib.rs +0 -87
  38. package/framework/core/rust-compiler/src/main_compiler.rs +0 -165
  39. package/framework/core/rust-compiler/src/parser.rs +0 -516
  40. package/framework/core/rust-compiler/src/ssr_generator.rs +0 -688
  41. package/framework/core/rust-compiler/src/transformer.rs +0 -562
  42. package/framework/core/rust-compiler/src/utils.rs +0 -10
  43. package/framework/engine-go/actions.go +0 -112
  44. package/framework/engine-go/cmd/server/main.go +0 -233
  45. package/framework/engine-go/comet/comet.go +0 -41
  46. package/framework/engine-go/comet/fragment.go +0 -51
  47. package/framework/engine-go/dispatcher.go +0 -120
  48. package/framework/engine-go/elements.go +0 -248
  49. package/framework/engine-go/evalexpr/evalexpr.go +0 -733
  50. package/framework/engine-go/go.mod +0 -3
  51. package/framework/engine-go/hydration.go +0 -43
  52. package/framework/engine-go/load-helpers.go +0 -29
  53. package/framework/engine-go/manifest.go +0 -41
  54. package/framework/engine-go/reactive.go +0 -123
  55. package/framework/engine-go/render.go +0 -635
  56. package/framework/engine-go/reqcontext.go +0 -48
  57. package/framework/engine-go/router.go +0 -85
  58. package/framework/engine-go/rustast/rustast.go +0 -317
  59. package/framework/engine-go/ssr.go +0 -103
  60. package/framework/engine-go/static-assets.go +0 -139
  61. package/framework/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -0,0 +1,445 @@
1
+ // Component runtime without the string-template compiler, so components with a compiled
2
+ // `render` (.fusee files, JSX) can leave it out of the bundle. component.js adds it back
3
+ // for defineComponent().
4
+ import { effect, setEffectHook, batch } from './signal.js'
5
+ import { NATIVE_SLOTS, insertFNode } from './h.js'
6
+ import { attachContextResponder, resolveProvided, requestContext, findInstanceHost } from './context.js'
7
+
8
+ // Slots context handed to compiled templates so hSlot() emits real <slot> outlets.
9
+ const NATIVE_SLOT_OUTLETS = Object.freeze({ [NATIVE_SLOTS]: true })
10
+ import { inject as diInject, rootInjector, runInContext, replaceActiveInjector, EnvironmentInjector } from './di.js'
11
+
12
+ let currentInstance = null
13
+
14
+ setEffectHook(eff => {
15
+ if (currentInstance) currentInstance._effects.push(eff)
16
+ })
17
+
18
+ /** Returns the component instance currently running setup or a lifecycle hook. */
19
+ export function getCurrentInstance() {
20
+ return currentInstance
21
+ }
22
+
23
+ /** Sets the active component instance for framework internals and composables. */
24
+ export function setCurrentInstance(instance) {
25
+ currentInstance = instance
26
+ }
27
+
28
+ // Global accessor hook for di.js execution context guard
29
+ globalThis.__FUSEE_GET_CURRENT_INSTANCE__ = getCurrentInstance
30
+
31
+ /**
32
+ * Resolves received props against a declared schema, validating types,
33
+ * filling in default values, and checking for missing required props.
34
+ */
35
+ function resolveProps(schema, received) {
36
+ const isArray = Array.isArray(schema)
37
+ const resolved = {}
38
+
39
+ const normalizedSchema = {}
40
+ if (isArray) {
41
+ schema.forEach(k => normalizedSchema[k.toLowerCase()] = k)
42
+ } else {
43
+ for (const key of Object.keys(schema)) {
44
+ normalizedSchema[key.toLowerCase()] = key
45
+ }
46
+ }
47
+
48
+ for (const receivedKey of Object.keys(received)) {
49
+ const schemaKey = isArray
50
+ ? schema.find(k => k.toLowerCase() === receivedKey.toLowerCase())
51
+ : normalizedSchema[receivedKey.toLowerCase()]
52
+
53
+ if (!schemaKey) {
54
+ console.warn(`[framework] Unknown prop "${receivedKey}"`)
55
+ continue
56
+ }
57
+
58
+ const descriptor = Object.getOwnPropertyDescriptor(received, receivedKey)
59
+ if (descriptor && descriptor.get) {
60
+ Object.defineProperty(resolved, schemaKey, {
61
+ get: descriptor.get,
62
+ enumerable: true,
63
+ configurable: true
64
+ })
65
+ } else {
66
+ resolved[schemaKey] = received[receivedKey]
67
+ }
68
+ }
69
+
70
+ if (!isArray) {
71
+ for (const [key, config] of Object.entries(schema)) {
72
+ if (!(key in resolved)) {
73
+ if (config.required) {
74
+ console.error(`[framework] Required prop "${key}" is missing`)
75
+ }
76
+ if (config.default !== undefined) {
77
+ resolved[key] = typeof config.default === 'function'
78
+ ? config.default()
79
+ : config.default
80
+ }
81
+ } else if (config.type) {
82
+ const value = resolved[key]
83
+ const expectedType = config.type.name
84
+ const actualType = typeof value
85
+ const typeMap = { String: 'string', Number: 'number', Boolean: 'boolean' }
86
+ if (typeMap[expectedType] && actualType !== typeMap[expectedType]) {
87
+ console.warn(`[framework] Prop "${key}" expected ${expectedType} but got ${actualType}`)
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ return resolved
94
+ }
95
+
96
+ /** Creates a batched event emitter that safely dispatches payloads to component listeners. */
97
+ function createEmit(listeners) {
98
+ return function emit(eventName, ...args) {
99
+ const handler = listeners[eventName]
100
+ if (typeof handler === 'function') {
101
+ batch(() => handler(...args))
102
+ } else if (handler !== undefined) {
103
+ console.warn(`[framework] emit("${eventName}"): listener is not a function`)
104
+ }
105
+ }
106
+ }
107
+
108
+ /** Parses named template slots and returns their content grouped by slot name. */
109
+ export function parseSlots(slotHTML) {
110
+ const slots = { default: '' }
111
+ if (!slotHTML || !slotHTML.trim()) return slots
112
+
113
+ const namedSlotRe = /<template\s+slot="([^"]+)"[^>]*>([\s\S]*?)<\/template>/gi
114
+ let remaining = slotHTML
115
+ let match
116
+
117
+ while ((match = namedSlotRe.exec(slotHTML)) !== null) {
118
+ slots[match[1]] = match[2].trim()
119
+ remaining = remaining.replace(match[0], '')
120
+ }
121
+
122
+ const defaultContent = remaining.trim()
123
+ if (defaultContent) slots.default = defaultContent
124
+
125
+ return slots
126
+ }
127
+
128
+ /** Replaces slot outlets in a component template with supplied slot content. */
129
+ function resolveSlots(template, slots) {
130
+ let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
131
+ return slots[name] ?? ''
132
+ })
133
+ result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
134
+ return slots.default ?? ''
135
+ })
136
+
137
+ result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
138
+ result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
139
+ return result
140
+ }
141
+
142
+ /** Registers a callback to run after the component has rendered. */
143
+ export function onMount(fn) {
144
+ if (currentInstance) currentInstance._mountHooks.push(fn)
145
+ }
146
+
147
+ /** Registers a callback to run when the component is unmounted. */
148
+ export function onUnmount(fn) {
149
+ if (currentInstance) currentInstance._unmountHooks.push(fn)
150
+ }
151
+
152
+ /**
153
+ * Defines a component whose markup comes from a compiled `render` function (.fusee files,
154
+ * JSX). Unlike defineComponent(), it does not bring the runtime template compiler into the
155
+ * bundle, so setup() must not return a string `template`.
156
+ */
157
+ export function defineCompiledComponent(options) {
158
+ return createComponentDefinition(options, null)
159
+ }
160
+
161
+ /**
162
+ * Creates a component factory with setup, rendering, props, slots, and lifecycle state.
163
+ * templateMounter renders string templates; it is null for compiled-only components.
164
+ */
165
+ export function createComponentDefinition(options, templateMounter) {
166
+ return Object.assign(function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
167
+ const effectiveParent = parent || currentInstance
168
+
169
+ const parentInjector = (effectiveParent && effectiveParent._injector) || rootInjector
170
+ const parentProvides = effectiveParent ? (effectiveParent.provides || effectiveParent._provides || null) : null
171
+ const parentApp = effectiveParent ? (effectiveParent._app || (effectiveParent._provides ? effectiveParent : null)) : (options._app || null)
172
+ const parentComponents = effectiveParent ? (effectiveParent._components || effectiveParent.components || null) : (parentApp ? (parentApp._components || parentApp.components || null) : {})
173
+
174
+ const rawMergedComponents = {
175
+ ...(parentComponents || {}),
176
+ ...(options.components || {})
177
+ }
178
+
179
+ const mergedComponents = new Proxy(rawMergedComponents, {
180
+ get(target, prop, receiver) {
181
+ if (typeof prop !== 'string') return Reflect.get(target, prop, receiver)
182
+ if (prop in target) return target[prop]
183
+ const lower = prop.toLowerCase()
184
+ for (const key of Object.keys(target)) {
185
+ if (key.toLowerCase() === lower) {
186
+ return target[key]
187
+ }
188
+ }
189
+ return undefined
190
+ },
191
+ has(target, prop) {
192
+ if (typeof prop !== 'string') return Reflect.has(target, prop)
193
+ if (prop in target) return true
194
+ const lower = prop.toLowerCase()
195
+ for (const key of Object.keys(target)) {
196
+ if (key.toLowerCase() === lower) return true
197
+ }
198
+ return false
199
+ }
200
+ })
201
+
202
+ const instance = {
203
+ props: options.props ? resolveProps(options.props, props) : props,
204
+ _mountHooks: [],
205
+ _unmountHooks: [],
206
+ _effects: [],
207
+ _element: null,
208
+ _parent: effectiveParent,
209
+ _app: parentApp,
210
+ _components: mergedComponents,
211
+ provides: parentProvides ? Object.create(parentProvides) : Object.create(null),
212
+ _injector: parentInjector,
213
+ _ownsInjector: false,
214
+ _host: options._host || null,
215
+ _detachContext: null
216
+ }
217
+
218
+ const emit = createEmit(listeners)
219
+
220
+ const previousInstance = currentInstance
221
+ currentInstance = instance
222
+ let result
223
+ try {
224
+ result = runInContext(instance._injector, () => {
225
+ return options.setup ? options.setup(instance.props, { emit, slots }) : {}
226
+ })
227
+ instance.state = result
228
+ if (result && typeof result === 'object') {
229
+ result._instance = instance
230
+ // _nativeSlots: rendered into a shadow root, where the browser distributes slots.
231
+ if (options._nativeSlots) result._slots = NATIVE_SLOT_OUTLETS
232
+ }
233
+ } finally {
234
+ currentInstance = previousInstance
235
+ }
236
+
237
+ function render(container) {
238
+ const prevInstance = currentInstance
239
+ currentInstance = instance
240
+ try {
241
+ instance._element = container
242
+ // Before mounting the template, so custom elements connecting during it can find this scope.
243
+ if (!instance._detachContext) instance._detachContext = attachContextResponder(container, instance)
244
+
245
+ if (options.render) {
246
+ const nodes = options.render(result, mergedComponents)
247
+ container.innerHTML = ''
248
+
249
+ for (const fnode of nodes) {
250
+ if (!fnode || !fnode.node) continue
251
+ insertFNode(container, fnode)
252
+ if (fnode.effects) instance._effects.push(...fnode.effects)
253
+ }
254
+ } else if (!templateMounter) {
255
+ if (result?.template) {
256
+ throw new Error('[fusée] defineCompiledComponent(): setup() returned a string `template`. Pass a compiled `render` function, or use defineComponent() for string templates.')
257
+ }
258
+ } else {
259
+ const template = result?.template || ''
260
+ const resolvedTemplate = options._nativeSlots ? template : resolveSlots(template, slots)
261
+ const { effects } = templateMounter(
262
+ resolvedTemplate,
263
+ container,
264
+ result,
265
+ mergedComponents
266
+ )
267
+ instance._effects.push(...effects)
268
+ }
269
+
270
+ for (const hook of instance._mountHooks)
271
+ hook()
272
+
273
+ return instance
274
+ } finally {
275
+ currentInstance = prevInstance
276
+ }
277
+ }
278
+
279
+ function unmount() {
280
+ for (const hook of instance._unmountHooks) hook()
281
+ const effects = instance._effects
282
+ for (let i = effects.length - 1; i >= 0; i--) {
283
+ const cleanup = effects[i]
284
+ if (typeof cleanup === 'function') cleanup()
285
+ }
286
+ instance._effects = []
287
+ if (instance._detachContext) {
288
+ instance._detachContext()
289
+ instance._detachContext = null
290
+ }
291
+ if (instance._element) {
292
+ instance._element.innerHTML = ''
293
+ }
294
+ }
295
+
296
+ return { render, unmount, instance }
297
+ }, { _options: options })
298
+ }
299
+
300
+ /** Provides a value or dependency injection configuration down to descendant components. */
301
+ export function provide(key, value) {
302
+ if (!currentInstance) return
303
+
304
+ const provideKey = (key && typeof key === 'object' && key.provide) ? key.provide : key
305
+ currentInstance.provides[provideKey] = value
306
+
307
+ if (!currentInstance._ownsInjector) {
308
+ currentInstance._injector = new EnvironmentInjector([], currentInstance._injector)
309
+ currentInstance._ownsInjector = true
310
+ replaceActiveInjector(currentInstance._injector)
311
+ }
312
+
313
+ if (value === undefined && key && key.provide) {
314
+ currentInstance._injector.provide(key)
315
+ } else if (value && typeof value === 'object' && (value.useClass || value.useFactory || value.useExisting)) {
316
+ currentInstance._injector.provide({ provide: key, ...value })
317
+ } else {
318
+ currentInstance._injector.provide({ provide: key, useValue: value })
319
+ }
320
+ }
321
+
322
+ /** Defines an asynchronous component wrapper that handles lazy loading, placeholders, and error states. */
323
+ export function defineAsyncComponent(loaderOrOptions) {
324
+ const options = typeof loaderOrOptions === 'function'
325
+ ? { loader: loaderOrOptions }
326
+ : loaderOrOptions
327
+
328
+ return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
329
+ const effectiveParent = parent || currentInstance
330
+ const parentInjector = (effectiveParent && effectiveParent._injector) || rootInjector
331
+ const parentProvides = effectiveParent
332
+ ? (effectiveParent.provides || effectiveParent._provides || null)
333
+ : null
334
+ const parentApp = effectiveParent ? effectiveParent._app : null
335
+ const parentComponents = effectiveParent ? effectiveParent._components : (parentApp ? parentApp._components : {})
336
+
337
+ const instance = {
338
+ props,
339
+ _mountHooks: [],
340
+ _unmountHooks: [],
341
+ _effects: [],
342
+ _element: null,
343
+ _parent: effectiveParent,
344
+ _app: parentApp,
345
+ _components: parentComponents,
346
+ provides: parentProvides ? Object.create(parentProvides) : Object.create(null),
347
+ _injector: parentInjector,
348
+ _ownsInjector: false
349
+ }
350
+
351
+ let childApi = null
352
+ let loadingApi = null
353
+ let isUnmounted = false
354
+
355
+ function render(container) {
356
+ const prevInstance = currentInstance
357
+ currentInstance = instance
358
+ try {
359
+ instance._element = container
360
+
361
+ if (options.loadingComponent) {
362
+ loadingApi = options.loadingComponent({}, { parent: instance })
363
+ loadingApi.render(container)
364
+ } else {
365
+ container.innerHTML = '<!-- async component boundary -->'
366
+ }
367
+
368
+ Promise.resolve(options.loader())
369
+ .then(comp => {
370
+ if (isUnmounted) return
371
+ let ComponentFn = comp.default || comp
372
+
373
+ if (typeof ComponentFn === 'object' && ComponentFn !== null) {
374
+ ComponentFn = Object.values(ComponentFn).find(v => typeof v === 'function') || ComponentFn
375
+ }
376
+
377
+ if (loadingApi) {
378
+ loadingApi.unmount()
379
+ loadingApi = null
380
+ }
381
+
382
+ childApi = ComponentFn(props, { listeners, slots, parent: instance })
383
+
384
+ container.innerHTML = ''
385
+ childApi.render(container)
386
+ })
387
+ .catch(err => {
388
+ console.error('[framework] Failed to load async component:', err)
389
+ })
390
+
391
+ return instance
392
+ } finally {
393
+ currentInstance = prevInstance
394
+ }
395
+ }
396
+
397
+ function unmount() {
398
+ isUnmounted = true
399
+ if (loadingApi) loadingApi.unmount()
400
+ if (childApi) childApi.unmount()
401
+ if (instance._element && !childApi && !loadingApi) {
402
+ instance._element.innerHTML = ''
403
+ }
404
+ }
405
+
406
+ return { render, unmount, instance }
407
+ }
408
+ }
409
+
410
+ /** Resolves and injects a provided value or dependency from the current component's provider tree or global injector. */
411
+ export function inject(key, defaultValue, options) {
412
+ const instance = getCurrentInstance()
413
+
414
+ let actualOptions = undefined
415
+ let actualDefaultValue = undefined
416
+
417
+ if (defaultValue && typeof defaultValue === 'object' &&
418
+ ('optional' in defaultValue || 'skipSelf' in defaultValue || 'self' in defaultValue)) {
419
+ actualOptions = defaultValue
420
+ } else {
421
+ actualDefaultValue = defaultValue
422
+ if (options && typeof options === 'object') {
423
+ actualOptions = options
424
+ }
425
+ }
426
+
427
+ const provided = resolveProvided(instance, key, actualOptions)
428
+ if (provided.found) return provided.value
429
+
430
+ // Inside a custom element, providers outside fusée (e.g. @lit/context) answer next.
431
+ const host = actualOptions?.self ? null : findInstanceHost(instance)
432
+ if (host) {
433
+ const requested = requestContext(host, key)
434
+ if (requested.found) return requested.value
435
+ }
436
+
437
+ try {
438
+ return diInject(key, actualOptions !== undefined ? actualOptions : actualDefaultValue)
439
+ } catch (err) {
440
+ if (actualDefaultValue !== undefined) {
441
+ return actualDefaultValue
442
+ }
443
+ throw err
444
+ }
445
+ }