jq79 0.3.30 → 0.3.32
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/dist/jq79.cjs +9 -8
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.global.js +9 -8
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +9 -8
- package/dist/jq79.js.map +1 -1
- package/dist/transform.d.ts +6 -0
- package/package.json +1 -1
- package/src/jq79.ts +27 -3
- package/src/transform.ts +187 -0
package/dist/transform.d.ts
CHANGED
|
@@ -3,5 +3,11 @@ type SetupTransform = {
|
|
|
3
3
|
code: string;
|
|
4
4
|
};
|
|
5
5
|
export declare const transformSetupScript: (src: string) => SetupTransform;
|
|
6
|
+
export type PropDecl = {
|
|
7
|
+
name: string;
|
|
8
|
+
default?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const parsePropsPattern: (pattern: string | undefined) => PropDecl[] | null;
|
|
11
|
+
export declare const parseFactoryProps: (src: string) => PropDecl[] | null;
|
|
6
12
|
export declare const transformFactoryScript: (src: string) => string | null;
|
|
7
13
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jq79",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.32",
|
|
4
4
|
"description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"reactive",
|
package/src/jq79.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { $, $$, $create, sanitizeHTML } from "./dom"
|
|
3
3
|
import { $reactive, untracked, createEffectScope } from "./reactive"
|
|
4
4
|
import type { ReactiveDeepData, EffectScope } from "./reactive"
|
|
5
|
-
import { transformSetupScript, transformFactoryScript } from "./transform"
|
|
5
|
+
import { transformSetupScript, transformFactoryScript, parsePropsPattern, parseFactoryProps, type PropDecl } from "./transform"
|
|
6
6
|
|
|
7
7
|
export { $, $$, $create } from "./dom"
|
|
8
8
|
export { $reactive } from "./reactive"
|
|
@@ -763,6 +763,25 @@ const runSetupScript = (code: string, scope: Record<string, any>, effect: (run:
|
|
|
763
763
|
result.catch(error => console.error("jq79: error in :setup script", error))
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
// puts a component's declared props on the store, before any script runs and
|
|
767
|
+
// before the first render: the names, so the template can bind to them even
|
|
768
|
+
// when the parent passes nothing, and the defaults, so it binds to something.
|
|
769
|
+
//
|
|
770
|
+
// A prop the parent *did* pass is already on the store (render() seeds it), so
|
|
771
|
+
// a default only fills an `undefined` - which is also what JS destructuring
|
|
772
|
+
// does with the same pattern, so both modes agree. It happens once, at setup:
|
|
773
|
+
// re-applying a default later would need an effect that reads and writes the
|
|
774
|
+
// same key, and that effect would wake itself forever.
|
|
775
|
+
//
|
|
776
|
+
// `null` props means the component declared no signature at all, which is not
|
|
777
|
+
// the same as declaring an empty one: it keeps today's permissive behavior
|
|
778
|
+
const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
|
|
779
|
+
props?.forEach(({ name, default: expr }) => {
|
|
780
|
+
if (store[name] !== undefined) return
|
|
781
|
+
store[name] = expr === undefined ? undefined : evalExpr(expr, store)
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
766
785
|
// default-import interop for factory scripts: real modules expose .default,
|
|
767
786
|
// while importing an .html component resolves to the Component79 itself
|
|
768
787
|
const interopDefault = (mod: any) => (mod && mod.default !== undefined ? mod.default : mod)
|
|
@@ -776,7 +795,7 @@ const interopDefault = (mod: any) => (mod && mod.default !== undefined ? mod.def
|
|
|
776
795
|
// resolve later and the template updates reactively
|
|
777
796
|
const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run: () => void) => void, instanceHelpers: Record<string, any> = {}, importer: (url: string) => Promise<any> = importResource, at: ScriptLocation = {}) => {
|
|
778
797
|
const helpers = { ...SETUP_HELPERS, ...instanceHelpers }
|
|
779
|
-
const $__exports: { default?: (ctx: Record<string, any>) => any; done?: boolean } = {}
|
|
798
|
+
const $__exports: { default?: (props: Record<string, any>, ctx: Record<string, any>) => any; done?: boolean } = {}
|
|
780
799
|
const result: Promise<void> = new Function(
|
|
781
800
|
"$__exports", "$__default", "$__import", ...Object.keys(helpers),
|
|
782
801
|
`return (async () => { "use strict";\n${code}\n;$__exports.done = true })()${sourceUrlComment(at.filename, at.index ?? 0)}`
|
|
@@ -795,7 +814,10 @@ const runFactoryScript = (code: string, scope: Record<string, any>, effect: (run
|
|
|
795
814
|
// the sync path is invoked straight from render(), so a throwing factory
|
|
796
815
|
// must be caught here too - not just by the `result` rejection handler
|
|
797
816
|
try {
|
|
798
|
-
|
|
817
|
+
// props first, ctx second. Both are the store: the pattern destructures
|
|
818
|
+
// the props it declared (copying, as destructuring does - $props is the
|
|
819
|
+
// live view for a primitive the parent reassigns later)
|
|
820
|
+
const returned = factory(scope, { $data: scope, $props: scope, $effect: effect, ...instanceHelpers })
|
|
799
821
|
if (returned instanceof Promise) returned.then(merge).catch(logError)
|
|
800
822
|
else merge(returned)
|
|
801
823
|
} catch (error) {
|
|
@@ -1100,11 +1122,13 @@ export class Component79 {
|
|
|
1100
1122
|
const at: ScriptLocation = { filename: this.filename, index }
|
|
1101
1123
|
const factoryCode = transformFactoryScript(script.content)
|
|
1102
1124
|
if (factoryCode !== null) {
|
|
1125
|
+
declareProps(store, parseFactoryProps(script.content))
|
|
1103
1126
|
const body = ":mounted" in script.attrs ? defer(factoryCode) : factoryCode
|
|
1104
1127
|
runFactoryScript(body, store, fx.effect, instanceHelpers, $import, at)
|
|
1105
1128
|
return
|
|
1106
1129
|
}
|
|
1107
1130
|
const { vars, code } = transformSetupScript(script.content)
|
|
1131
|
+
declareProps(store, parsePropsPattern(script.attrs[":setup"]))
|
|
1108
1132
|
// pre-declare script vars on the store so `with` resolves assignments
|
|
1109
1133
|
// to them (and reads of them) through the reactive proxy
|
|
1110
1134
|
vars.forEach(name => { if (!(name in store)) (store as any)[name] = undefined })
|
package/src/transform.ts
CHANGED
|
@@ -235,6 +235,193 @@ const staticImportToAwait = (clause: string | undefined, spec: string, n: number
|
|
|
235
235
|
return `const ${bindings.join(", ")}`
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// the component's prop signature
|
|
240
|
+
//
|
|
241
|
+
// A component declares the props it takes as a destructuring pattern, in the
|
|
242
|
+
// place each script mode already puts its inputs: the `:setup` attribute's
|
|
243
|
+
// value, or the factory's *first* parameter (the ctx moved to the second).
|
|
244
|
+
// Position is fixed, so the signature is read straight from the source string -
|
|
245
|
+
// no parser, no execution - and the runtime can seed the defaults on the store
|
|
246
|
+
// before the first render, which is what makes them reach the template even in
|
|
247
|
+
// factory mode (where JS would only apply them inside the function body).
|
|
248
|
+
//
|
|
249
|
+
// <script :setup="{ label = 'Total', step = 1 }">
|
|
250
|
+
// export default ({ label = "Total" }, { $data }) => {}
|
|
251
|
+
//
|
|
252
|
+
// An object pattern *is* the declaration; anything else (`_`, a plain
|
|
253
|
+
// identifier, no attribute at all) declares nothing and stays permissive.
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
export type PropDecl = { name: string; default?: string }
|
|
257
|
+
|
|
258
|
+
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/
|
|
259
|
+
|
|
260
|
+
// index of the first `ch` at bracket depth 0, skipping strings and comments
|
|
261
|
+
const indexOfTopLevel = (src: string, ch: string): number => {
|
|
262
|
+
let depth = 0
|
|
263
|
+
let i = 0
|
|
264
|
+
while (i < src.length) {
|
|
265
|
+
const c = src[i]
|
|
266
|
+
if (c === "'" || c === '"' || c === "`") { i = skipString(src, i); continue }
|
|
267
|
+
if (c === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
268
|
+
if (c === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
269
|
+
if ("([{".includes(c)) depth++
|
|
270
|
+
else if (")]}".includes(c)) depth--
|
|
271
|
+
else if (depth === 0 && c === ch) return i
|
|
272
|
+
i++
|
|
273
|
+
}
|
|
274
|
+
return -1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const splitTopLevel = (src: string): string[] => {
|
|
278
|
+
const parts: string[] = []
|
|
279
|
+
let depth = 0
|
|
280
|
+
let start = 0
|
|
281
|
+
let i = 0
|
|
282
|
+
while (i <= src.length) {
|
|
283
|
+
const ch = src[i]
|
|
284
|
+
if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); continue }
|
|
285
|
+
if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
286
|
+
if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
287
|
+
if (ch !== undefined && "([{".includes(ch)) depth++
|
|
288
|
+
else if (ch !== undefined && ")]}".includes(ch)) depth--
|
|
289
|
+
else if (i === src.length || (ch === "," && depth === 0)) {
|
|
290
|
+
const part = src.slice(start, i).trim()
|
|
291
|
+
if (part) parts.push(part)
|
|
292
|
+
start = i + 1
|
|
293
|
+
}
|
|
294
|
+
i++
|
|
295
|
+
}
|
|
296
|
+
return parts
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// the `=` that opens a default value: the first one at depth 0 that isn't part
|
|
300
|
+
// of `==`/`===` or an arrow's `=>` (so `{ format = a => a }` keeps its default)
|
|
301
|
+
const defaultAssignIndex = (src: string): number => {
|
|
302
|
+
let from = 0
|
|
303
|
+
while (from < src.length) {
|
|
304
|
+
const at = indexOfTopLevel(src.slice(from), "=")
|
|
305
|
+
if (at === -1) return -1
|
|
306
|
+
const i = from + at
|
|
307
|
+
if (src[i + 1] !== "=" && src[i + 1] !== ">" && src[i - 1] !== "=" && src[i - 1] !== "!") return i
|
|
308
|
+
from = i + 1
|
|
309
|
+
}
|
|
310
|
+
return -1
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// a destructuring pattern -> the props it declares. null means "no signature":
|
|
314
|
+
// the pattern isn't an object one (`_`, `props`, nothing at all), so the
|
|
315
|
+
// component declares nothing and keeps the permissive, undeclared behavior.
|
|
316
|
+
// `{}` parses to [] - a closed signature that declares zero props
|
|
317
|
+
export const parsePropsPattern = (pattern: string | undefined): PropDecl[] | null => {
|
|
318
|
+
const src = (pattern ?? "").trim()
|
|
319
|
+
if (!src.startsWith("{")) return null
|
|
320
|
+
|
|
321
|
+
// to the `}` that closes the pattern, so a parameter's own default value
|
|
322
|
+
// (`({ label } = {})`) is left out of it
|
|
323
|
+
let depth = 0
|
|
324
|
+
let close = 0
|
|
325
|
+
while (close < src.length) {
|
|
326
|
+
const ch = src[close]
|
|
327
|
+
if (ch === "'" || ch === '"' || ch === "`") { close = skipString(src, close); continue }
|
|
328
|
+
if ("([{".includes(ch)) depth++
|
|
329
|
+
else if (")]}".includes(ch) && --depth === 0) break
|
|
330
|
+
close++
|
|
331
|
+
}
|
|
332
|
+
if (close >= src.length) return null // unbalanced: not a pattern we can read
|
|
333
|
+
|
|
334
|
+
const props: PropDecl[] = []
|
|
335
|
+
for (const part of splitTopLevel(src.slice(1, close))) {
|
|
336
|
+
if (part.startsWith("...")) continue // a rest element names no prop
|
|
337
|
+
const assign = defaultAssignIndex(part)
|
|
338
|
+
const named = assign === -1 ? part : part.slice(0, assign)
|
|
339
|
+
const fallback = assign === -1 ? undefined : part.slice(assign + 1).trim()
|
|
340
|
+
// `{ user: { id } }` and `{ user: renamed }` both declare `user`: what the
|
|
341
|
+
// store holds is the key, whatever the pattern binds it to
|
|
342
|
+
const colon = indexOfTopLevel(named, ":")
|
|
343
|
+
const name = (colon === -1 ? named : named.slice(0, colon)).trim()
|
|
344
|
+
if (!IDENTIFIER_RE.test(name)) continue
|
|
345
|
+
props.push(fallback === undefined ? { name } : { name, default: fallback })
|
|
346
|
+
}
|
|
347
|
+
return props
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// index just past a top-level `export default`, or -1
|
|
351
|
+
const findExportDefault = (src: string): number => {
|
|
352
|
+
let i = 0
|
|
353
|
+
let depth = 0
|
|
354
|
+
let atStatementStart = true
|
|
355
|
+
while (i < src.length) {
|
|
356
|
+
const ch = src[i]
|
|
357
|
+
if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); atStatementStart = false; continue }
|
|
358
|
+
if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
|
|
359
|
+
if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
|
|
360
|
+
|
|
361
|
+
if (ch === "e" && depth === 0 && atStatementStart && (i === 0 || !/[\w$.]/.test(src[i - 1]))) {
|
|
362
|
+
EXPORT_DEFAULT_RE.lastIndex = i
|
|
363
|
+
const found = EXPORT_DEFAULT_RE.exec(src)
|
|
364
|
+
if (found) return i + found[0].length
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if ("([{".includes(ch)) depth++
|
|
368
|
+
else if (")]}".includes(ch)) depth = Math.max(0, depth - 1)
|
|
369
|
+
if (ch === "\n" || ch === ";" || ch === "}") atStatementStart = true
|
|
370
|
+
else if (!/\s/.test(ch)) atStatementStart = false
|
|
371
|
+
i++
|
|
372
|
+
}
|
|
373
|
+
return -1
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const ASYNC_RE = /^async(?![\w$])/
|
|
377
|
+
const FUNCTION_RE = /^function(?![\w$])\s*\*?\s*[A-Za-z_$][\w$]*|^function(?![\w$])\s*\*?/
|
|
378
|
+
|
|
379
|
+
// source text of the exported function's first parameter, or null when there
|
|
380
|
+
// is no parameter list to read (`export default Factory`, `export default
|
|
381
|
+
// props => ...`, an exported object) - all of which declare nothing
|
|
382
|
+
const firstParameterSource = (src: string): string | null => {
|
|
383
|
+
const start = findExportDefault(src)
|
|
384
|
+
if (start === -1) return null
|
|
385
|
+
|
|
386
|
+
let i = skipToToken(src, start)
|
|
387
|
+
const rest = src.slice(i)
|
|
388
|
+
if (ASYNC_RE.test(rest)) i = skipToToken(src, i + "async".length)
|
|
389
|
+
const fn = FUNCTION_RE.exec(src.slice(i))
|
|
390
|
+
if (fn) i = skipToToken(src, i + fn[0].length)
|
|
391
|
+
if (src[i] !== "(") return null
|
|
392
|
+
|
|
393
|
+
// the parameter list runs to the `)` that closes this `(`
|
|
394
|
+
let depth = 0
|
|
395
|
+
let end = i
|
|
396
|
+
while (end < src.length) {
|
|
397
|
+
const ch = src[end]
|
|
398
|
+
if (ch === "'" || ch === '"' || ch === "`") { end = skipString(src, end); continue }
|
|
399
|
+
if ("([{".includes(ch)) depth++
|
|
400
|
+
else if (")]}".includes(ch) && --depth === 0) break
|
|
401
|
+
end++
|
|
402
|
+
}
|
|
403
|
+
return splitTopLevel(src.slice(i + 1, end))[0] ?? ""
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// the props declared by a factory script's first parameter. Throws the
|
|
407
|
+
// migration error when it finds the pre-0.4 signature there - the ctx used to
|
|
408
|
+
// be the first parameter, and the change is silent otherwise ($data would just
|
|
409
|
+
// come back undefined). `$` is what tells them apart: what carries one comes
|
|
410
|
+
// from the library, what doesn't comes from the parent, everywhere in jq79
|
|
411
|
+
export const parseFactoryProps = (src: string): PropDecl[] | null => {
|
|
412
|
+
const first = firstParameterSource(src)
|
|
413
|
+
if (first === null) return null
|
|
414
|
+
const props = parsePropsPattern(first)
|
|
415
|
+
const ctxName = props?.find(prop => prop.name.startsWith("$"))?.name
|
|
416
|
+
if (ctxName) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`jq79: the factory signature is (props, ctx), so \`${ctxName}\` can't be destructured from the first parameter. ` +
|
|
419
|
+
`Write \`export default (props, { ${ctxName} }) => …\`, or \`_\` in place of props if the component takes none.`
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
return props
|
|
423
|
+
}
|
|
424
|
+
|
|
238
425
|
// rewrites a factory script into a Function body, or returns null when the
|
|
239
426
|
// script has no top-level `export default` (i.e. it's a regular setup script)
|
|
240
427
|
export const transformFactoryScript = (src: string): string | null => {
|