jq79 0.2.0 → 0.3.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/README.md +43 -271
- package/assets/Component79.svg +8 -0
- package/dist/dom.d.ts +3 -0
- package/dist/jq79.cjs +9 -5
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +8 -17
- package/dist/jq79.global.js +9 -5
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +9 -5
- package/dist/jq79.js.map +1 -1
- package/dist/reactive.d.ts +20 -0
- package/dist/transform.d.ts +7 -0
- package/dist/vite.cjs +116 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +92 -0
- package/dist/vite.js.map +1 -0
- package/package.json +24 -4
- package/src/dom.ts +33 -0
- package/src/jq79.ts +95 -367
- package/src/reactive.ts +187 -0
- package/src/transform.ts +282 -0
- package/src/vite.ts +154 -0
package/src/jq79.ts
CHANGED
|
@@ -1,35 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const $$ = (selectorOrEl: string | Element, selector?: string) => Array.from(
|
|
8
|
-
typeof selectorOrEl === "string"
|
|
9
|
-
? document.querySelectorAll(selectorOrEl)
|
|
10
|
-
: selectorOrEl.querySelectorAll(selector || "")
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// $create(tag, attrs): attrs are set as attributes, except className, which
|
|
14
|
-
// may be a string or an array of class names.
|
|
15
|
-
export const $create = (tag: string, attrs: Record<string, any> = {}): HTMLElement => {
|
|
16
|
-
const el = document.createElement(tag);
|
|
17
|
-
for (const [name, value] of Object.entries(attrs)) {
|
|
18
|
-
if (name === 'className') {
|
|
19
|
-
el.className = Array.isArray(value) ? value.join(' ') : value;
|
|
20
|
-
} else if (name === 'textContent') {
|
|
21
|
-
el.textContent = value;
|
|
22
|
-
} else if (name === 'children') {
|
|
23
|
-
for (const child of value) {
|
|
24
|
-
el.appendChild(child);
|
|
25
|
-
}
|
|
26
|
-
} else {
|
|
27
|
-
el.setAttribute(name, value);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return el;
|
|
31
|
-
};
|
|
2
|
+
import { $, $$, $create } from "./dom"
|
|
3
|
+
import { $reactive, untracked, createEffectScope } from "./reactive"
|
|
4
|
+
import type { ReactiveDeepData, EffectScope } from "./reactive"
|
|
5
|
+
import { transformSetupScript, transformFactoryScript } from "./transform"
|
|
32
6
|
|
|
7
|
+
export { $, $$, $create } from "./dom"
|
|
8
|
+
export { $reactive } from "./reactive"
|
|
33
9
|
type TemplateNode = {
|
|
34
10
|
tag: string
|
|
35
11
|
attrs: Record<string, string>
|
|
@@ -79,191 +55,12 @@ const evalExpr = (expr: string, scope: Record<string, any>, extras?: Record<stri
|
|
|
79
55
|
const interpolate = (template: string, scope: Record<string, any>): string =>
|
|
80
56
|
template.replace(/{{\s*(.+?)\s*}}/g, (_, expr) => evalExpr(expr, scope) ?? "")
|
|
81
57
|
|
|
82
|
-
type ChangeListener = (value: any, dotKey: string) => void
|
|
83
|
-
type AnyChangeListener = (dotKey: string, value: any) => void
|
|
84
|
-
type ListenerOptions = { immediate?: boolean }
|
|
85
|
-
type Unsubscribe = () => void
|
|
86
|
-
|
|
87
|
-
type ReactiveDeepData<T> = T & {
|
|
88
|
-
$on: (dotKey: string, listener: ChangeListener, options?: ListenerOptions) => Unsubscribe
|
|
89
|
-
$onAny: (listener: AnyChangeListener, options?: ListenerOptions) => Unsubscribe
|
|
90
|
-
// runs `run` immediately, recording every dotKey it reads off this store, then
|
|
91
|
-
// re-runs it whenever a changed dotKey overlaps one of those - see pathsOverlap
|
|
92
|
-
$effect: (run: () => void) => Unsubscribe
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const getByPath = (obj: Record<string, any>, dotKey: string): any =>
|
|
96
|
-
dotKey.split(".").reduce((acc, key) => (acc == null ? undefined : acc[key]), obj)
|
|
97
|
-
|
|
98
|
-
// only plain objects and arrays get deep-wrapped by the reactive store;
|
|
99
|
-
// class instances (Component79, Date, DOM nodes, ...) pass through untouched
|
|
100
|
-
// so their identity, prototypes and internals stay intact
|
|
101
|
-
const isPlainData = (value: object): boolean => {
|
|
102
|
-
if (Array.isArray(value)) return true
|
|
103
|
-
const proto = Object.getPrototypeOf(value)
|
|
104
|
-
return proto === Object.prototype || proto === null
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const walkLeaves = (obj: Record<string, any>, path: string, visit: (dotKey: string, value: any) => void) => {
|
|
108
|
-
Object.entries(obj).forEach(([key, value]) => {
|
|
109
|
-
const dotKey = path ? `${path}.${key}` : key
|
|
110
|
-
if (value && typeof value === "object" && isPlainData(value)) walkLeaves(value, dotKey, visit)
|
|
111
|
-
else visit(dotKey, value)
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// true when `a` and `b` sit on the same ancestor/descendant line, e.g.
|
|
116
|
-
// "user" & "user.address.city" (a change to either affects the other) - false
|
|
117
|
-
// for siblings like "user.name" & "user.age"
|
|
118
|
-
const pathsOverlap = (a: string, b: string): boolean =>
|
|
119
|
-
a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`)
|
|
120
|
-
|
|
121
|
-
// active $effect() runs, innermost last - a module-level stack (rather than
|
|
122
|
-
// one per store) so nested effects across stores still nest correctly; reads
|
|
123
|
-
// during makeReactive's `get` trap are attributed to whichever run is on top
|
|
124
|
-
const trackerStack: Set<string>[] = []
|
|
125
|
-
|
|
126
|
-
// runs fn with dependency tracking suspended - reads inside it are attributed
|
|
127
|
-
// to a throwaway set instead of the currently running effect
|
|
128
|
-
const untracked = <T>(fn: () => T): T => {
|
|
129
|
-
trackerStack.push(new Set())
|
|
130
|
-
try {
|
|
131
|
-
return fn()
|
|
132
|
-
} finally {
|
|
133
|
-
trackerStack.pop()
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
type Effect = { deps: Set<string>; run: () => void }
|
|
138
|
-
|
|
139
|
-
export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepData<T> => {
|
|
140
|
-
const exactListeners = new Map<string, Set<ChangeListener>>()
|
|
141
|
-
const anyListeners = new Set<AnyChangeListener>()
|
|
142
|
-
const effects = new Set<Effect>()
|
|
143
|
-
// every proxy this store has ever handed out, so re-assigning an object
|
|
144
|
-
// that's already reactive (e.g. `data.list = [data.list[1], data.list[0]]`)
|
|
145
|
-
// doesn't wrap it a second time - which would hand out a *new* object
|
|
146
|
-
// identity for the same logical item, breaking reference-equality checks
|
|
147
|
-
// like :each's keyed diffing
|
|
148
|
-
const reactiveProxies = new WeakSet<object>()
|
|
149
|
-
|
|
150
|
-
const notify = (dotKey: string, value: any) => {
|
|
151
|
-
exactListeners.get(dotKey)?.forEach(listener => listener(value, dotKey))
|
|
152
|
-
anyListeners.forEach(listener => listener(dotKey, value))
|
|
153
|
-
effects.forEach(effect => {
|
|
154
|
-
if (Array.from(effect.deps).some(dep => pathsOverlap(dep, dotKey))) effect.run()
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const makeReactive = (obj: Record<string, any>, path: string): Record<string, any> => {
|
|
159
|
-
if (reactiveProxies.has(obj)) return obj
|
|
160
|
-
|
|
161
|
-
Object.entries(obj).forEach(([key, value]) => {
|
|
162
|
-
if (value && typeof value === "object" && isPlainData(value)) {
|
|
163
|
-
obj[key] = makeReactive(value, path ? `${path}.${key}` : key)
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const proxy = new Proxy(obj, {
|
|
168
|
-
get(target, key, receiver) {
|
|
169
|
-
if (typeof key === "string") {
|
|
170
|
-
trackerStack[trackerStack.length - 1]?.add(path ? `${path}.${key}` : key)
|
|
171
|
-
}
|
|
172
|
-
return Reflect.get(target, key, receiver)
|
|
173
|
-
},
|
|
174
|
-
set(target, key: string, value, receiver) {
|
|
175
|
-
// an assignment delegated up the prototype chain from a derived scope
|
|
176
|
-
// (Object.create(store) child, or a wrapping proxy): if the key isn't
|
|
177
|
-
// a real property of this store, honor the receiver so the new binding
|
|
178
|
-
// lands on the derived scope - a scope-local variable, not a store
|
|
179
|
-
// mutation, so no notify. If the key IS a store property, fall through
|
|
180
|
-
// and mutate the store itself regardless of receiver, so assignments
|
|
181
|
-
// like @click="count = count + 1" work from any nested scope
|
|
182
|
-
if (receiver !== proxy && !Object.prototype.hasOwnProperty.call(target, key)) {
|
|
183
|
-
return Reflect.set(target, key, value, receiver)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const dotKey = path ? `${path}.${key}` : key
|
|
187
|
-
if (value && typeof value === "object" && isPlainData(value)) {
|
|
188
|
-
value = makeReactive(value, dotKey)
|
|
189
|
-
}
|
|
190
|
-
target[key] = value
|
|
191
|
-
notify(dotKey, value)
|
|
192
|
-
return true
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
reactiveProxies.add(proxy)
|
|
197
|
-
return proxy
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const reactive = makeReactive(data, "") as ReactiveDeepData<T>
|
|
201
|
-
|
|
202
|
-
const $on = (dotKey: string, listener: ChangeListener, { immediate = false }: ListenerOptions = {}): Unsubscribe => {
|
|
203
|
-
if (!exactListeners.has(dotKey)) exactListeners.set(dotKey, new Set())
|
|
204
|
-
exactListeners.get(dotKey)!.add(listener)
|
|
205
|
-
if (immediate) listener(getByPath(reactive, dotKey), dotKey)
|
|
206
|
-
return () => exactListeners.get(dotKey)?.delete(listener)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const $onAny = (listener: AnyChangeListener, { immediate = false }: ListenerOptions = {}): Unsubscribe => {
|
|
210
|
-
anyListeners.add(listener)
|
|
211
|
-
if (immediate) walkLeaves(reactive, "", (dotKey, value) => listener(dotKey, value))
|
|
212
|
-
return () => anyListeners.delete(listener)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const $effect = (run: () => void): Unsubscribe => {
|
|
216
|
-
const effect: Effect = {
|
|
217
|
-
deps: new Set(),
|
|
218
|
-
run: () => {
|
|
219
|
-
const deps = new Set<string>()
|
|
220
|
-
trackerStack.push(deps)
|
|
221
|
-
try {
|
|
222
|
-
run()
|
|
223
|
-
} finally {
|
|
224
|
-
trackerStack.pop()
|
|
225
|
-
effect.deps = deps
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
}
|
|
229
|
-
effects.add(effect)
|
|
230
|
-
effect.run()
|
|
231
|
-
return () => { effects.delete(effect) }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
Object.defineProperty(reactive, "$on", { value: $on, enumerable: false })
|
|
235
|
-
Object.defineProperty(reactive, "$onAny", { value: $onAny, enumerable: false })
|
|
236
|
-
Object.defineProperty(reactive, "$effect", { value: $effect, enumerable: false })
|
|
237
|
-
|
|
238
|
-
return reactive
|
|
239
|
-
}
|
|
240
58
|
|
|
241
59
|
const CONTROL_ATTRS = new Set([":bind", ":if", ":elseif", ":else", ":each", ":key", ":with"])
|
|
242
60
|
const EACH_PATTERN = /^\s*(\w+)\s+in\s+(.+)$/
|
|
243
61
|
|
|
244
62
|
type ConditionalBranch = { expr?: string; node: TemplateNode }
|
|
245
63
|
|
|
246
|
-
// groups the disposers of every $effect created for one rendered subtree
|
|
247
|
-
// (an :if branch, an :each item, ...) so the whole subtree's bindings can be
|
|
248
|
-
// torn down in one call when that subtree is replaced/removed. `scope.$effect`
|
|
249
|
-
// resolves through the prototype chain up to the root store no matter how
|
|
250
|
-
// many nested :each scopes sit in between (see renderEach's itemScope)
|
|
251
|
-
type EffectScope = {
|
|
252
|
-
effect: (run: () => void) => void
|
|
253
|
-
// registers an arbitrary cleanup (e.g. destroying a nested component) to
|
|
254
|
-
// run when this subtree is torn down
|
|
255
|
-
onDispose: (fn: Unsubscribe) => void
|
|
256
|
-
dispose: () => void
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const createEffectScope = (scope: Record<string, any>): EffectScope => {
|
|
260
|
-
const disposers: Unsubscribe[] = []
|
|
261
|
-
return {
|
|
262
|
-
effect: run => { disposers.push(scope.$effect(run)) },
|
|
263
|
-
onDispose: fn => { disposers.push(fn) },
|
|
264
|
-
dispose: () => { disposers.splice(0).forEach(dispose => dispose()) },
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
64
|
|
|
268
65
|
// @event attributes: @click="onClick", @submit.prevent="$event => onSubmit($event)",
|
|
269
66
|
// or an inline statement like @click="count = count + 1". The expression is
|
|
@@ -342,9 +139,9 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
342
139
|
currentDef = nextDef
|
|
343
140
|
if (!nextDef) return
|
|
344
141
|
|
|
345
|
-
// a fresh instance per usage site: the definition's parsed parts
|
|
346
|
-
// shared, but store/effects/DOM are per instance
|
|
347
|
-
const instance = new Component79({ template: nextDef.template, scripts: nextDef.scripts, styles: nextDef.styles })
|
|
142
|
+
// a fresh instance per usage site: the definition's parsed parts (and
|
|
143
|
+
// pre-resolved modules) are shared, but store/effects/DOM are per instance
|
|
144
|
+
const instance = new Component79({ template: nextDef.template, scripts: nextDef.scripts, styles: nextDef.styles, modules: nextDef.modules })
|
|
348
145
|
const seed = untracked(() =>
|
|
349
146
|
Object.fromEntries(Object.entries(props).map(([name, expr]) => [name, evalExpr(expr, scope)]))
|
|
350
147
|
)
|
|
@@ -421,6 +218,22 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
421
218
|
|
|
422
219
|
const el = document.createElement(node.tag)
|
|
423
220
|
|
|
221
|
+
// a tag that isn't standard HTML but has no matching scope key *yet* may be
|
|
222
|
+
// a component that arrives later (e.g. an async factory script exposing an
|
|
223
|
+
// imported component after `await`). Watch for the key: the effect tracks
|
|
224
|
+
// no deps, so it only re-runs on the store's new-key sweep, and swaps the
|
|
225
|
+
// placeholder element for the component exactly once
|
|
226
|
+
if (el instanceof HTMLUnknownElement || node.tag.includes("-")) {
|
|
227
|
+
let upgraded = false
|
|
228
|
+
fx.effect(() => {
|
|
229
|
+
if (upgraded) return
|
|
230
|
+
const key = findComponentKey(scope, node.tag)
|
|
231
|
+
if (!key) return
|
|
232
|
+
upgraded = true
|
|
233
|
+
el.replaceWith(renderNestedComponent(key, node, scope, fx))
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
424
237
|
Object.entries(node.attrs).forEach(([key, value]) => {
|
|
425
238
|
if (key.startsWith("@")) bindEvent(el, key, value, scope)
|
|
426
239
|
else if (!CONTROL_ATTRS.has(key)) el.setAttribute(key, value)
|
|
@@ -612,6 +425,10 @@ type ComponentParts = {
|
|
|
612
425
|
template: TemplateNode[]
|
|
613
426
|
scripts: TagBlock[]
|
|
614
427
|
styles: TagBlock[]
|
|
428
|
+
// pre-resolved modules for `import(...)` calls in setup scripts, keyed by
|
|
429
|
+
// the literal specifier. Bundlers (the jq79/vite plugin) fill this so
|
|
430
|
+
// imports resolve from the bundle instead of being fetched at runtime
|
|
431
|
+
modules?: Record<string, any>
|
|
615
432
|
}
|
|
616
433
|
|
|
617
434
|
const VOID_ELEMENTS = new Set([
|
|
@@ -681,152 +498,6 @@ const parseComponentString = (component: string): ComponentParts => {
|
|
|
681
498
|
return { template, scripts, styles }
|
|
682
499
|
}
|
|
683
500
|
|
|
684
|
-
// ---------------------------------------------------------------------------
|
|
685
|
-
// :setup script transform
|
|
686
|
-
//
|
|
687
|
-
// setup scripts are written like Svelte components:
|
|
688
|
-
//
|
|
689
|
-
// let firstName = null
|
|
690
|
-
// $: fullName = `${firstName} ${lastName}`
|
|
691
|
-
// fetchUser().then(user => { firstName = user.firstName })
|
|
692
|
-
//
|
|
693
|
-
// and are executed inside `with ($scope)` against the component's reactive
|
|
694
|
-
// store, so plain assignments (even from async callbacks) go through the
|
|
695
|
-
// proxy's set trap and re-render whatever depends on them. To make that work
|
|
696
|
-
// the source is lightly rewritten - no full JS parser, just a scanner that is
|
|
697
|
-
// string/comment-aware and only touches code at brace/paren depth 0:
|
|
698
|
-
// - `let/var/const x = ...` at the top level loses its keyword, becoming a
|
|
699
|
-
// scope assignment (the name is pre-declared on the store so the `with`
|
|
700
|
-
// lookup resolves it)
|
|
701
|
-
// - `$: x = expr` becomes `$__effect(() => { x = expr })`, re-running when a
|
|
702
|
-
// dependency read inside expr changes ($__effect is deliberately NOT a
|
|
703
|
-
// property of the scope, so `with` falls through to the function parameter)
|
|
704
|
-
// ---------------------------------------------------------------------------
|
|
705
|
-
|
|
706
|
-
type SetupTransform = { vars: string[]; code: string }
|
|
707
|
-
|
|
708
|
-
const DECLARATION_RE = /(?:let|var|const)\s+([A-Za-z_$][\w$]*)/y
|
|
709
|
-
const REACTIVE_LABEL_RE = /\$:\s*/y
|
|
710
|
-
const IMPORT_CALL_RE = /import(?=\s*\()/y
|
|
711
|
-
const REACTIVE_ASSIGN_RE = /\$:\s*([A-Za-z_$][\w$]*)\s*=(?!=)/y
|
|
712
|
-
|
|
713
|
-
const skipString = (src: string, start: number): number => {
|
|
714
|
-
const quote = src[start]
|
|
715
|
-
let i = start + 1
|
|
716
|
-
while (i < src.length) {
|
|
717
|
-
if (src[i] === "\\") { i += 2; continue }
|
|
718
|
-
if (src[i] === quote) return i + 1
|
|
719
|
-
i++
|
|
720
|
-
}
|
|
721
|
-
return src.length
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const skipLineComment = (src: string, start: number): number => {
|
|
725
|
-
const end = src.indexOf("\n", start)
|
|
726
|
-
return end === -1 ? src.length : end
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const skipBlockComment = (src: string, start: number): number => {
|
|
730
|
-
const end = src.indexOf("*/", start + 2)
|
|
731
|
-
return end === -1 ? src.length : end + 2
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// end of a statement starting at `start`: the first newline or `;` that isn't
|
|
735
|
-
// inside a string/comment or unbalanced brackets, so multi-line RHS like a
|
|
736
|
-
// wrapped function call or template literal stays in one piece
|
|
737
|
-
const findStatementEnd = (src: string, start: number): number => {
|
|
738
|
-
let depth = 0
|
|
739
|
-
let i = start
|
|
740
|
-
while (i < src.length) {
|
|
741
|
-
const ch = src[i]
|
|
742
|
-
if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); continue }
|
|
743
|
-
if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
744
|
-
if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
745
|
-
if ("([{".includes(ch)) depth++
|
|
746
|
-
else if (")]}".includes(ch)) depth--
|
|
747
|
-
else if (depth <= 0 && (ch === "\n" || ch === ";")) return i
|
|
748
|
-
i++
|
|
749
|
-
}
|
|
750
|
-
return src.length
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const transformSetupScript = (src: string): SetupTransform => {
|
|
754
|
-
const vars: string[] = []
|
|
755
|
-
let out = ""
|
|
756
|
-
let i = 0
|
|
757
|
-
let depth = 0
|
|
758
|
-
let atStatementStart = true
|
|
759
|
-
|
|
760
|
-
while (i < src.length) {
|
|
761
|
-
const ch = src[i]
|
|
762
|
-
const next = src[i + 1]
|
|
763
|
-
|
|
764
|
-
if (ch === "'" || ch === '"' || ch === "`") {
|
|
765
|
-
const end = skipString(src, i)
|
|
766
|
-
out += src.slice(i, end)
|
|
767
|
-
i = end
|
|
768
|
-
atStatementStart = false
|
|
769
|
-
continue
|
|
770
|
-
}
|
|
771
|
-
if (ch === "/" && (next === "/" || next === "*")) {
|
|
772
|
-
const end = next === "/" ? skipLineComment(src, i) : skipBlockComment(src, i)
|
|
773
|
-
out += src.slice(i, end)
|
|
774
|
-
i = end
|
|
775
|
-
continue
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// `import(...)` is a keyword form, so it can't be intercepted through the
|
|
779
|
-
// scope - rewrite the identifier to the injected $__import (which loads
|
|
780
|
-
// .html URLs as components via Component79.fetch and delegates the rest to
|
|
781
|
-
// native import). The `(` is left for the scanner so depth stays balanced
|
|
782
|
-
if (ch === "i" && (i === 0 || !/[\w$.]/.test(src[i - 1]))) {
|
|
783
|
-
IMPORT_CALL_RE.lastIndex = i
|
|
784
|
-
if (IMPORT_CALL_RE.test(src)) {
|
|
785
|
-
out += "$__import"
|
|
786
|
-
i += "import".length
|
|
787
|
-
atStatementStart = false
|
|
788
|
-
continue
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (depth === 0 && atStatementStart) {
|
|
793
|
-
DECLARATION_RE.lastIndex = i
|
|
794
|
-
const decl = DECLARATION_RE.exec(src)
|
|
795
|
-
if (decl) {
|
|
796
|
-
vars.push(decl[1])
|
|
797
|
-
out += decl[1]
|
|
798
|
-
i += decl[0].length
|
|
799
|
-
atStatementStart = false
|
|
800
|
-
continue
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
REACTIVE_LABEL_RE.lastIndex = i
|
|
804
|
-
const label = REACTIVE_LABEL_RE.exec(src)
|
|
805
|
-
if (label) {
|
|
806
|
-
REACTIVE_ASSIGN_RE.lastIndex = i
|
|
807
|
-
const assign = REACTIVE_ASSIGN_RE.exec(src)
|
|
808
|
-
if (assign) vars.push(assign[1])
|
|
809
|
-
const start = i + label[0].length
|
|
810
|
-
const end = findStatementEnd(src, start)
|
|
811
|
-
out += `$__effect(() => { ${src.slice(start, end)} });`
|
|
812
|
-
i = end
|
|
813
|
-
continue
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if ("([{".includes(ch)) depth++
|
|
818
|
-
else if (")]}".includes(ch)) depth = Math.max(0, depth - 1)
|
|
819
|
-
|
|
820
|
-
if (ch === "\n" || ch === ";" || ch === "}") atStatementStart = true
|
|
821
|
-
else if (!/\s/.test(ch)) atStatementStart = false
|
|
822
|
-
|
|
823
|
-
out += ch
|
|
824
|
-
i++
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return { vars, code: out }
|
|
828
|
-
}
|
|
829
|
-
|
|
830
501
|
// loads .html URLs as components, delegating anything else to native import()
|
|
831
502
|
const importResource = (url: string): Promise<any> =>
|
|
832
503
|
/\.html?([?#]|$)/.test(url) ? Component79.fetch(url) : import(url)
|
|
@@ -872,7 +543,7 @@ const SETUP_HELPERS: Record<string, any> = { $, $$, $create, $reactive }
|
|
|
872
543
|
// The body is wrapped in an async IIFE so top-level `await` works: everything
|
|
873
544
|
// up to the first await runs synchronously (before the template renders), and
|
|
874
545
|
// later assignments update the DOM reactively when they happen
|
|
875
|
-
const runSetupScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}) => {
|
|
546
|
+
const runSetupScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource) => {
|
|
876
547
|
// instanceHelpers are per-component-instance additions (e.g. $emit, which
|
|
877
548
|
// is bound to this instance's DOM position)
|
|
878
549
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
@@ -884,10 +555,48 @@ const runSetupScript = (code: string, scope: Record<string, any>, effect: (run:
|
|
|
884
555
|
const result: Promise<void> = new Function(
|
|
885
556
|
"$scope", "$__effect", "$__import", ...Object.keys(helpers),
|
|
886
557
|
`return (async () => { with ($scope) { ${code} } })()`
|
|
887
|
-
)(scriptScope, effect,
|
|
558
|
+
)(scriptScope, effect, importer, ...Object.values(helpers))
|
|
888
559
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
889
560
|
}
|
|
890
561
|
|
|
562
|
+
// default-import interop for factory scripts: real modules expose .default,
|
|
563
|
+
// while importing an .html component resolves to the Component79 itself
|
|
564
|
+
const interopDefault = (mod: any) => (mod && mod.default !== undefined ? mod.default : mod)
|
|
565
|
+
|
|
566
|
+
// runs a factory script: the (rewritten) module body executes in plain
|
|
567
|
+
// lexical strict-mode scope - no `with`, no implicit reactivity - with the
|
|
568
|
+
// library helpers as parameters, then the default export is called with the
|
|
569
|
+
// instance context and a returned object is merged into the store. A fully
|
|
570
|
+
// synchronous body invokes the factory before the first render, matching
|
|
571
|
+
// setup-script timing; bodies with top-level await (static imports included)
|
|
572
|
+
// resolve later and the template updates reactively
|
|
573
|
+
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource) => {
|
|
574
|
+
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
575
|
+
const $__exports: { default?: (ctx: Record<string, any>) => any; done?: boolean } = {}
|
|
576
|
+
const result: Promise<void> = new Function(
|
|
577
|
+
"$__exports", "$__default", "$__import", ...Object.keys(helpers),
|
|
578
|
+
`return (async () => { "use strict";\n${code}\n;$__exports.done = true })()`
|
|
579
|
+
)($__exports, interopDefault, importer, ...Object.values(helpers))
|
|
580
|
+
|
|
581
|
+
const logError = (error: any) => console.error("jq79: error in factory script", error)
|
|
582
|
+
let invoked = false
|
|
583
|
+
const invoke = () => {
|
|
584
|
+
if (invoked) return
|
|
585
|
+
invoked = true
|
|
586
|
+
const factory = $__exports.default
|
|
587
|
+
if (typeof factory !== "function") return
|
|
588
|
+
const merge = (bindings: any) => {
|
|
589
|
+
if (bindings && typeof bindings === "object") Object.assign(scope, bindings)
|
|
590
|
+
}
|
|
591
|
+
const returned = factory({ $data: scope, $effect: effect, ...instanceHelpers })
|
|
592
|
+
if (returned instanceof Promise) returned.then(merge).catch(logError)
|
|
593
|
+
else merge(returned)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
result.then(invoke, logError)
|
|
597
|
+
if ($__exports.done) invoke() // fully-sync body: factory runs before first render
|
|
598
|
+
}
|
|
599
|
+
|
|
891
600
|
type EmitListener = (event: CustomEvent, payload: any) => void
|
|
892
601
|
|
|
893
602
|
// a parsed single-file component. Typical lifecycle:
|
|
@@ -902,6 +611,9 @@ export class Component79 {
|
|
|
902
611
|
template: TemplateNode[]
|
|
903
612
|
scripts: TagBlock[]
|
|
904
613
|
styles: TagBlock[]
|
|
614
|
+
// pre-resolved modules for setup-script `import(...)` calls (see
|
|
615
|
+
// ComponentParts.modules); checked before falling back to fetch/import
|
|
616
|
+
modules?: Record<string, any>
|
|
905
617
|
|
|
906
618
|
data: ReactiveDeepData<Record<string, any>> | null = null
|
|
907
619
|
|
|
@@ -926,11 +638,12 @@ export class Component79 {
|
|
|
926
638
|
// outside the render generation so they survive re-render and destroy()
|
|
927
639
|
private emitListeners = new Map<string, Set<EmitListener>>()
|
|
928
640
|
|
|
929
|
-
constructor(src: string | ComponentParts) {
|
|
930
|
-
const
|
|
931
|
-
this.template = template
|
|
932
|
-
this.scripts = scripts
|
|
933
|
-
this.styles = styles
|
|
641
|
+
constructor(src: string | ComponentParts, options: { modules?: Record<string, any> } = {}) {
|
|
642
|
+
const parts = typeof src === "string" ? parseComponentString(src) : src
|
|
643
|
+
this.template = parts.template
|
|
644
|
+
this.scripts = parts.scripts
|
|
645
|
+
this.styles = parts.styles
|
|
646
|
+
this.modules = options.modules ?? (typeof src === "string" ? undefined : src.modules)
|
|
934
647
|
}
|
|
935
648
|
|
|
936
649
|
static async fetch(url: string): Promise<Component79> {
|
|
@@ -1021,15 +734,30 @@ export class Component79 {
|
|
|
1021
734
|
}
|
|
1022
735
|
const $self = (selector: string): Element | null => $$self(selector)[0] ?? null
|
|
1023
736
|
|
|
737
|
+
// import() calls whose specifier was pre-resolved by a bundler (the
|
|
738
|
+
// modules map) get the bundled module; everything else falls back to the
|
|
739
|
+
// runtime importResource (fetch for .html, native import otherwise)
|
|
740
|
+
const modules = this.modules
|
|
741
|
+
const $import = (url: string): Promise<any> =>
|
|
742
|
+
modules && url in modules ? Promise.resolve(modules[url]) : importResource(url)
|
|
743
|
+
|
|
1024
744
|
// scripts run before the template renders so `$:` values are initialized;
|
|
1025
|
-
// a `:mounted` script defers entirely until mount() instead
|
|
745
|
+
// a `:mounted` script defers entirely until mount() instead. A top-level
|
|
746
|
+
// `export default` switches the script to factory mode (plain lexical JS)
|
|
1026
747
|
this.scripts.forEach(script => {
|
|
748
|
+
const instanceHelpers = { $emit, $mounted, $self, $$self }
|
|
749
|
+
const factoryCode = transformFactoryScript(script.content)
|
|
750
|
+
if (factoryCode !== null) {
|
|
751
|
+
const body = ":mounted" in script.attrs ? `await $mounted();\n${factoryCode}` : factoryCode
|
|
752
|
+
runFactoryScript(body, store, fx.effect, instanceHelpers, $import)
|
|
753
|
+
return
|
|
754
|
+
}
|
|
1027
755
|
const { vars, code } = transformSetupScript(script.content)
|
|
1028
756
|
// pre-declare script vars on the store so `with` resolves assignments
|
|
1029
757
|
// to them (and reads of them) through the reactive proxy
|
|
1030
758
|
vars.forEach(name => { if (!(name in store)) (store as any)[name] = undefined })
|
|
1031
759
|
const body = ":mounted" in script.attrs ? `await $mounted();\n${code}` : code
|
|
1032
|
-
runSetupScript(body, store, fx.effect,
|
|
760
|
+
runSetupScript(body, store, fx.effect, instanceHelpers, $import)
|
|
1033
761
|
})
|
|
1034
762
|
|
|
1035
763
|
const content = document.createDocumentFragment()
|