fusee-framework 1.2.1 → 1.4.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,158 @@
1
+ import { counterLogic, mouseTracker } from '../composables/demo.js'
2
+ import { useFetch } from '../composables/useFetch.js'
3
+ import { useLocalStorage } from '../composables/useLocalStorage.js'
4
+
5
+ export const TestComposables = defineComponent({
6
+ setup() {
7
+ // ── 1. counterLogic — pure signal composable, no context needed ──────
8
+ const counter = counterLogic(0, 1)
9
+ const bigCounter = counterLogic(100, 10)
10
+
11
+ // ── 2. mouseTracker — lifecycle composable (needs setup context) ─────
12
+ const mouse = mouseTracker()
13
+
14
+ // ── 3. useLocalStorage — persists across page reloads ────────────────
15
+ const theme = useLocalStorage('demo-theme', 'dark')
16
+ const nickname = useLocalStorage('demo-nickname', '')
17
+
18
+ const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
19
+
20
+ // ── 4. useFetch — reactive data fetching ─────────────────────────────
21
+ const { data: posts, loading, error } = useFetch(
22
+ 'https://jsonplaceholder.typicode.com/posts?_limit=3'
23
+ )
24
+ // ── 5. Custom Directives Test ────────────────────────────────────────
25
+ const hColor = signal('#8b8bff33')
26
+ const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
27
+ let colorIdx = 0
28
+ const nextColor = () => hColor(colors[++colorIdx % colors.length])
29
+
30
+ return {
31
+ hColor,
32
+ nextColor,
33
+
34
+ // counter
35
+ count: counter.count,
36
+ doubled: counter.doubled,
37
+ isEven: counter.isEven,
38
+ increment: counter.increment,
39
+ decrement: counter.decrement,
40
+ reset: counter.reset,
41
+
42
+ // big counter
43
+ bigCount: bigCounter.count,
44
+ bigIncrement: bigCounter.increment,
45
+ bigDecrement: bigCounter.decrement,
46
+
47
+ // mouse
48
+ mouseX: mouse.x,
49
+ mouseY: mouse.y,
50
+
51
+ // localStorage
52
+ theme,
53
+ nickname,
54
+ toggleTheme,
55
+
56
+ // fetch
57
+ posts,
58
+ loading,
59
+ error,
60
+
61
+ template: `
62
+ <div class="page">
63
+ <h1>🧩 Composables Demo</h1>
64
+ <p style="color:#888; margin-bottom: 2rem;">
65
+ Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
66
+ No naming rules — just reactive logic.
67
+ </p>
68
+
69
+ <!-- ── 1. counterLogic ─────────────────────────────────────── -->
70
+ <section class="demo-card">
71
+ <h2>📦 counterLogic(0, step=1)</h2>
72
+ <p class="demo-desc">Pure signal composable — works without setup() context.</p>
73
+ <div class="counter-display">
74
+ <span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
75
+ <span class="derived-label">× 2 = {{ doubled }}</span>
76
+ </div>
77
+ <div class="btn-row">
78
+ <button @click="decrement" class="cmp-btn minus">−</button>
79
+ <button @click="reset" class="cmp-btn reset">↺</button>
80
+ <button @click="increment" class="cmp-btn plus">+</button>
81
+ </div>
82
+ <p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
83
+ </section>
84
+
85
+ <!-- ── 1b. Second independent counter (step=10) ───────────── -->
86
+ <section class="demo-card">
87
+ <h2>📦 counterLogic(100, step=10)</h2>
88
+ <p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
89
+ <div class="counter-display">
90
+ <span class="big-num">{{ bigCount }}</span>
91
+ </div>
92
+ <div class="btn-row">
93
+ <button @click="bigDecrement" class="cmp-btn minus">−10</button>
94
+ <button @click="bigIncrement" class="cmp-btn plus">+10</button>
95
+ </div>
96
+ </section>
97
+
98
+ <!-- ── 2. mouseTracker ────────────────────────────────────── -->
99
+ <section class="demo-card">
100
+ <h2>🖱️ mouseTracker()</h2>
101
+ <p class="demo-desc">Lifecycle composable — registers window listener on mount, cleans up on unmount.</p>
102
+ <div class="mouse-display">
103
+ <span>X: <strong>{{ mouseX }}</strong></span>
104
+ <span>Y: <strong>{{ mouseY }}</strong></span>
105
+ </div>
106
+ </section>
107
+
108
+ <!-- ── 3. useLocalStorage ─────────────────────────────────── -->
109
+ <section class="demo-card">
110
+ <h2>💾 useLocalStorage()</h2>
111
+ <p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
112
+ <div style="margin-bottom: 12px;">
113
+ <input f-model="nickname"
114
+ class="cmp-input"
115
+ placeholder="Your nickname…" />
116
+ <p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
117
+ Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
118
+ </p>
119
+ </div>
120
+ <button @click="toggleTheme" class="cmp-btn reset">
121
+ Theme: {{ theme }}
122
+ </button>
123
+ </section>
124
+
125
+ <!-- ── 4. useFetch ────────────────────────────────────────── -->
126
+ <section class="demo-card">
127
+ <h2>🌐 useFetch()</h2>
128
+ <p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
129
+ <div f-if="loading">
130
+ <p style="color: #888;">⏳ Loading posts…</p>
131
+ </div>
132
+ <div f-elif="error">
133
+ <p style="color: #ff6b6b;">❌ {{ error }}</p>
134
+ </div>
135
+ <div f-else>
136
+ <div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
137
+ <strong style="color:#ff9999">{{ post.title }}</strong>
138
+ <p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
139
+ </div>
140
+ </div>
141
+ </section>
142
+
143
+ <!-- ── 6. Custom Directives ────────────────────────────────────── -->
144
+ <section class="demo-card">
145
+ <h2>🛠️ Custom Directives</h2>
146
+ <p class="demo-desc">Testing <code>f-focus</code> and <code>f-highlight</code>.</p>
147
+
148
+ <div f-highlight="hColor" style="padding: 15px; border-radius: 8px; margin-bottom: 10px;">
149
+ <input f-focus class="cmp-input" placeholder="I have f-focus!" />
150
+ </div>
151
+
152
+ <button @click="nextColor" class="cmp-btn plus">Change Highlight Color</button>
153
+ </section>
154
+ </div>
155
+ `
156
+ }
157
+ }
158
+ })
@@ -0,0 +1,158 @@
1
+ import { defineComponent } from '../../framework/index.js'
2
+ import { counterLogic, mouseTracker } from '../composables/demo.js'
3
+ import { useFetch } from '../composables/useFetch.js'
4
+ import { useLocalStorage } from '../composables/useLocalStorage.js'
5
+
6
+ export const TestComposables = defineComponent({
7
+ setup() {
8
+ // ── 1. counterLogic — pure signal composable, no context needed ──────
9
+ const counter = counterLogic(0, 1)
10
+ const bigCounter = counterLogic(100, 10)
11
+
12
+ // ── 2. mouseTracker — lifecycle composable (needs setup context) ─────
13
+ const mouse = mouseTracker()
14
+
15
+ // ── 3. useLocalStorage — persists across page reloads ────────────────
16
+ const theme = useLocalStorage<string>('demo-theme', 'dark')
17
+ const nickname = useLocalStorage<string>('demo-nickname', '')
18
+
19
+ const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
20
+
21
+ // ── 4. useFetch — reactive data fetching ─────────────────────────────
22
+ const { data: posts, loading, error } = useFetch(
23
+ 'https://jsonplaceholder.typicode.com/posts?_limit=3'
24
+ )
25
+ // ── 5. Custom Directives Test ────────────────────────────────────────
26
+ const hColor = signal('#8b8bff33')
27
+ const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
28
+ let colorIdx = 0
29
+ const nextColor = () => hColor(colors[++colorIdx % colors.length])
30
+
31
+ return {
32
+ hColor,
33
+ nextColor,
34
+
35
+ // counter
36
+ count: counter.count,
37
+ doubled: counter.doubled,
38
+ isEven: counter.isEven,
39
+ increment: counter.increment,
40
+ decrement: counter.decrement,
41
+ reset: counter.reset,
42
+
43
+ // big counter
44
+ bigCount: bigCounter.count,
45
+ bigIncrement: bigCounter.increment,
46
+ bigDecrement: bigCounter.decrement,
47
+
48
+ // mouse
49
+ mouseX: mouse.x,
50
+ mouseY: mouse.y,
51
+
52
+ // localStorage
53
+ theme,
54
+ nickname,
55
+ toggleTheme,
56
+
57
+ // fetch
58
+ posts,
59
+ loading,
60
+ error,
61
+
62
+ template: `
63
+ <div class="page">
64
+ <h1>🧩 Composables Demo</h1>
65
+ <p style="color:#888; margin-bottom: 2rem;">
66
+ Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
67
+ No naming rules — just reactive logic.
68
+ </p>
69
+
70
+ <!-- ── 1. counterLogic ─────────────────────────────────────── -->
71
+ <section class="demo-card">
72
+ <h2>📦 counterLogic(0, step=1)</h2>
73
+ <p class="demo-desc">Pure signal composable — works without setup() context.</p>
74
+ <div class="counter-display">
75
+ <span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
76
+ <span class="derived-label">× 2 = {{ doubled }}</span>
77
+ </div>
78
+ <div class="btn-row">
79
+ <button @click="decrement" class="cmp-btn minus">−</button>
80
+ <button @click="reset" class="cmp-btn reset">↺</button>
81
+ <button @click="increment" class="cmp-btn plus">+</button>
82
+ </div>
83
+ <p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
84
+ </section>
85
+
86
+ <!-- ── 1b. Second independent counter (step=10) ───────────── -->
87
+ <section class="demo-card">
88
+ <h2>📦 counterLogic(100, step=10)</h2>
89
+ <p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
90
+ <div class="counter-display">
91
+ <span class="big-num">{{ bigCount }}</span>
92
+ </div>
93
+ <div class="btn-row">
94
+ <button @click="bigDecrement" class="cmp-btn minus">−10</button>
95
+ <button @click="bigIncrement" class="cmp-btn plus">+10</button>
96
+ </div>
97
+ </section>
98
+
99
+ <!-- ── 2. mouseTracker ────────────────────────────────────── -->
100
+ <section class="demo-card">
101
+ <h2>🖱️ mouseTracker()</h2>
102
+ <p class="demo-desc">Lifecycle composable — registers window listener on mount, cleans up on unmount.</p>
103
+ <div class="mouse-display">
104
+ <span>X: <strong>{{ mouseX }}</strong></span>
105
+ <span>Y: <strong>{{ mouseY }}</strong></span>
106
+ </div>
107
+ </section>
108
+
109
+ <!-- ── 3. useLocalStorage ─────────────────────────────────── -->
110
+ <section class="demo-card">
111
+ <h2>💾 useLocalStorage()</h2>
112
+ <p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
113
+ <div style="margin-bottom: 12px;">
114
+ <input f-model="nickname"
115
+ class="cmp-input"
116
+ placeholder="Your nickname…" />
117
+ <p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
118
+ Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
119
+ </p>
120
+ </div>
121
+ <button @click="toggleTheme" class="cmp-btn reset">
122
+ Theme: {{ theme }}
123
+ </button>
124
+ </section>
125
+
126
+ <!-- ── 4. useFetch ────────────────────────────────────────── -->
127
+ <section class="demo-card">
128
+ <h2>🌐 useFetch()</h2>
129
+ <p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
130
+ <div f-if="loading">
131
+ <p style="color: #888;">⏳ Loading posts…</p>
132
+ </div>
133
+ <div f-elif="error">
134
+ <p style="color: #ff6b6b;">❌ {{ error }}</p>
135
+ </div>
136
+ <div f-else>
137
+ <div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
138
+ <strong style="color:#ff9999">{{ post.title }}</strong>
139
+ <p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
140
+ </div>
141
+ </div>
142
+ </section>
143
+ <!-- ── 6. Custom Directives ────────────────────────────────────── -->
144
+ <section class="demo-card">
145
+ <h2>🛠️ Custom Directives</h2>
146
+ <p class="demo-desc">Testing <code>f-focus</code> and <code>f-highlight</code>.</p>
147
+
148
+ <div f-highlight="hColor" style="padding: 15px; border-radius: 8px; margin-bottom: 10px;">
149
+ <input f-focus class="cmp-input" placeholder="I have f-focus!" />
150
+ </div>
151
+
152
+ <button @click="nextColor" class="cmp-btn plus">Change Highlight Color</button>
153
+ </section>
154
+ </div>
155
+ `
156
+ }
157
+ }
158
+ })
@@ -0,0 +1,40 @@
1
+ import { DynamicA } from '../components/DynamicA.js'
2
+ import { DynamicB } from '../components/DynamicB.js'
3
+
4
+ export const TestDynamic = defineComponent({
5
+ components: { DynamicA, DynamicB },
6
+ setup() {
7
+ const currentType = signal('DynamicA')
8
+ const sharedTitle = signal('Test Dinamic active')
9
+
10
+ return {
11
+ currentType,
12
+ sharedTitle,
13
+ toggle: () => {
14
+ currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
15
+ },
16
+ template: `
17
+ <div class="page">
18
+ <h1>Dynamic Components System (Vue-style)</h1>
19
+ <p>Using the custom <code>f-is</code> directive.</p>
20
+
21
+ <div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
22
+ <button @click="toggle" class="primary-btn">Toggle Component</button>
23
+ <span>Active: <strong>{{ currentType }}</strong></span>
24
+ </div>
25
+
26
+ <input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
27
+
28
+ <hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
29
+
30
+ <div f-is="currentType" f-keepAlive :title="sharedTitle"></div>
31
+
32
+ <p style="margin-top: 40px; color: #888; font-size: 0.9em;">
33
+ Note: The component above changes reactively when you click the button.
34
+ The <code>:title</code> prop is automatically passed to the active component.
35
+ </p>
36
+ </div>
37
+ `
38
+ }
39
+ }
40
+ })
@@ -0,0 +1,40 @@
1
+ import { DynamicA } from '../components/DynamicA.js'
2
+ import { DynamicB } from '../components/DynamicB.js'
3
+
4
+ export const TestDynamic = defineComponent({
5
+ components: { DynamicA, DynamicB },
6
+ setup() {
7
+ const currentType = signal('DynamicA')
8
+ const sharedTitle = signal('Dynamic Test Active')
9
+
10
+ return {
11
+ currentType,
12
+ sharedTitle,
13
+ toggle: () => {
14
+ currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
15
+ },
16
+ template: `
17
+ <div class="page">
18
+ <h1>Dynamic Components System (Vue-style)</h1>
19
+ <p>Using the custom <code>f-is</code> directive.</p>
20
+
21
+ <div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
22
+ <button @click="toggle" class="primary-btn">Toggle Component</button>
23
+ <span>Active: <strong>{{ currentType }}</strong></span>
24
+ </div>
25
+
26
+ <input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
27
+
28
+ <hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
29
+
30
+ <div f-is="currentType" :title="sharedTitle"></div>
31
+
32
+ <p style="margin-top: 40px; color: #888; font-size: 0.9em;">
33
+ Note: The component above changes reactively when you click the button.
34
+ The <code>:title</code> prop is automatically passed to the active component.
35
+ </p>
36
+ </div>
37
+ `
38
+ }
39
+ }
40
+ })
@@ -11,11 +11,10 @@ export const TestFor = defineComponent({
11
11
  items,
12
12
  addItem: () => {
13
13
  const nextId = Math.max(0, ...items().map(i => i.id)) + 1
14
- items([...items(), { id: nextId, name: `Item ${nextId}` }])
14
+ items.push({ id: nextId, name: `Item ${nextId}` })
15
15
  },
16
16
  shuffleItems: () => {
17
- const shuffled = [...items()].sort(() => Math.random() - 0.5)
18
- items(shuffled)
17
+ items.sort(() => Math.random() - 0.5)
19
18
  },
20
19
  template: `
21
20
  <div class="page">
@@ -23,11 +23,10 @@ export const TestFor = defineComponent({
23
23
  addItem: () => {
24
24
  const nextIds = items().map(i => i.id)
25
25
  const nextId = nextIds.length > 0 ? Math.max(...nextIds) + 1 : 1
26
- items([...items(), { id: nextId, name: `Item ${nextId}` }])
26
+ items.push({ id: nextId, name: `Item ${nextId}` })
27
27
  },
28
28
  shuffleItems: () => {
29
- const shuffled = [...items()].sort(() => Math.random() - 0.5)
30
- items(shuffled)
29
+ items.sort(() => Math.random() - 0.5)
31
30
  },
32
31
  template: `
33
32
  <div class="page">
package/auto-imports.d.ts CHANGED
@@ -9,6 +9,7 @@ declare global {
9
9
  const batch: typeof import('fusee-framework').batch
10
10
  const computed: typeof import('fusee-framework').computed
11
11
  const defineComponent: typeof import('fusee-framework').defineComponent
12
+ const directive: typeof import('fusee-framework').directive
12
13
  const effect: typeof import('fusee-framework').effect
13
14
  const emit: typeof import('fusee-framework').emit
14
15
  const inject: typeof import('fusee-framework').inject
@@ -3,6 +3,7 @@ export const FuseePreset = {
3
3
  'signal', 'computed', 'effect',
4
4
  'batch', 'untrack', 'inspect',
5
5
  'defineComponent', 'onMount', 'onUnmount',
6
+ 'directive',
6
7
  'mountOutlet', 'mountTemplate', 'emit'
7
8
  ]
8
9
  }
@@ -7,6 +7,10 @@ setEffectHook(eff => {
7
7
  if (currentInstance) currentInstance._effects.push(eff)
8
8
  })
9
9
 
10
+ export function getCurrentInstance() {
11
+ return currentInstance
12
+ }
13
+
10
14
  function resolveProps(schema, received) {
11
15
  const isArray = Array.isArray(schema)
12
16
  const resolved = {}
@@ -68,9 +72,6 @@ function resolveProps(schema, received) {
68
72
  return resolved
69
73
  }
70
74
 
71
- // ── Emit ──────────────────────────────────────────────────────────────────────
72
- // listeners: { change: fn, submit: fn, ... }
73
- // emit('change', 42) → calls listeners.change(42)
74
75
  function createEmit(listeners) {
75
76
  return function emit(eventName, ...args) {
76
77
  const handler = listeners[eventName]
@@ -82,10 +83,6 @@ function createEmit(listeners) {
82
83
  }
83
84
  }
84
85
 
85
- // ── Slots ─────────────────────────────────────────────────────────────────────
86
- // Parses slot content from parent HTML string.
87
- // Named slots: <template slot="header">...</template>
88
- // Default slot: everything else
89
86
  export function parseSlots(slotHTML) {
90
87
  const slots = { default: '' }
91
88
  if (!slotHTML || !slotHTML.trim()) return slots
@@ -105,19 +102,15 @@ export function parseSlots(slotHTML) {
105
102
  return slots
106
103
  }
107
104
 
108
- // Replaces <slot> and <slot name="x"> in template with actual slot content
109
105
  function resolveSlots(template, slots) {
110
- // Handle named slots with any content between tags (including default content)
111
106
  let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
112
107
  return slots[name] ?? ''
113
108
  })
114
- // Handle default slots with any content between tags
115
109
  result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
116
110
  return slots.default ?? ''
117
111
  })
118
- // Handle self-closing named slots
112
+
119
113
  result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
120
- // Handle self-closing default slots
121
114
  result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
122
115
  return result
123
116
  }
@@ -131,10 +124,6 @@ export function onUnmount(fn) {
131
124
  }
132
125
 
133
126
  export function defineComponent(options) {
134
- // ComponentFactory now accepts props + optional { listeners, slots, parent }
135
- // listeners: event handlers from parent @eventName="handler"
136
- // slots: parsed slot HTML from parent
137
- // parent: parent component instance for provide/inject
138
127
  return function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
139
128
  const instance = {
140
129
  props: options.props ? resolveProps(options.props, props) : props,
@@ -149,16 +138,13 @@ export function defineComponent(options) {
149
138
  const emit = createEmit(listeners)
150
139
 
151
140
  currentInstance = instance
152
- // setup() receives (props, { emit, slots })
153
141
  const result = options.setup(instance.props, { emit, slots })
154
- // Store instance reference on result for child component access
155
142
  result._instance = instance
156
143
  currentInstance = null
157
144
 
158
145
  function render(container) {
159
146
  instance._element = container
160
147
 
161
- // Resolve slot placeholders before mounting
162
148
  const resolvedTemplate = resolveSlots(result.template, slots)
163
149
 
164
150
  const { effects } = mountTemplate(
@@ -187,15 +173,84 @@ export function defineComponent(options) {
187
173
  }
188
174
 
189
175
  export function provide(key, value) {
190
- if(!currentInstance) return
176
+ if (!currentInstance) return
191
177
  currentInstance._provides[key] = value
192
178
  }
193
179
 
194
180
  export function inject(key) {
195
181
  let parent = currentInstance?._parent
196
- while(parent) {
182
+ while (parent) {
197
183
  if (key in parent._provides) return parent._provides[key];
198
184
  parent = parent._parent;
199
185
  }
200
186
  return null;
187
+ }
188
+
189
+ export function defineAsyncComponent(loaderOrOptions) {
190
+ const options = typeof loaderOrOptions === 'function'
191
+ ? { loader: loaderOrOptions }
192
+ : loaderOrOptions
193
+
194
+ return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
195
+ const instance = {
196
+ props,
197
+ _mountHooks: [],
198
+ _unmountHooks: [],
199
+ _effects: [],
200
+ _element: null,
201
+ _provides: {},
202
+ _parent: parent,
203
+ }
204
+
205
+ let childApi = null
206
+ let loadingApi = null
207
+ let isUnmounted = false
208
+
209
+ function render(container) {
210
+ instance._element = container
211
+
212
+ if (options.loadingComponent) {
213
+ loadingApi = options.loadingComponent({}, { parent })
214
+ loadingApi.render(container)
215
+ } else {
216
+ container.innerHTML = '<!-- async component boundary -->'
217
+ }
218
+
219
+ Promise.resolve(options.loader())
220
+ .then(comp => {
221
+ if (isUnmounted) return
222
+ let ComponentFn = comp.default || comp
223
+
224
+ if (typeof ComponentFn === 'object' && ComponentFn !== null) {
225
+ ComponentFn = Object.values(ComponentFn).find(v => typeof v === 'function') || ComponentFn
226
+ }
227
+
228
+ if (loadingApi) {
229
+ loadingApi.unmount()
230
+ loadingApi = null
231
+ }
232
+
233
+ childApi = ComponentFn(props, { listeners, slots, parent })
234
+
235
+ container.innerHTML = ''
236
+ childApi.render(container)
237
+ })
238
+ .catch(err => {
239
+ console.error('[framework] Failed to load async component:', err)
240
+ })
241
+
242
+ return instance
243
+ }
244
+
245
+ function unmount() {
246
+ isUnmounted = true
247
+ if (loadingApi) loadingApi.unmount()
248
+ if (childApi) childApi.unmount()
249
+ if (instance._element && !childApi && !loadingApi) {
250
+ instance._element.innerHTML = ''
251
+ }
252
+ }
253
+
254
+ return { render, unmount, instance }
255
+ }
201
256
  }
@@ -0,0 +1,27 @@
1
+ import { getCurrentInstance } from './component.js'
2
+
3
+ export function defineComposable(fn) {
4
+ const composable = function (...args) {
5
+ return fn(...args)
6
+ }
7
+
8
+ composable._isComposable = true
9
+
10
+ Object.defineProperty(composable, 'name', {
11
+ value: fn.name || 'composable',
12
+ configurable: true
13
+ })
14
+
15
+ return composable
16
+ }
17
+
18
+ export function assertSetupContext(name) {
19
+ const instance = getCurrentInstance()
20
+ if (!instance) {
21
+ console.warn(
22
+ `[framework] ${name ? name + ' ' : ''}composable was called outside of setup(). ` +
23
+ `Lifecycle hooks (onMount, onUnmount) and injections will not work.`
24
+ )
25
+ }
26
+ return instance
27
+ }