jq79 0.5.0 → 0.5.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/dist/jq79.cjs +13 -13
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.global.js +13 -13
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +13 -13
- package/dist/jq79.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +110 -13
package/package.json
CHANGED
package/src/jq79.ts
CHANGED
|
@@ -689,6 +689,7 @@ const renderNestedComponent = (key: string, node: TemplateNode, scope: Record<st
|
|
|
689
689
|
// or an undeclared name would be filtered on the first render and reappear
|
|
690
690
|
// on the next update
|
|
691
691
|
const declared = declaredPropSet(instance.scripts)
|
|
692
|
+
warnUndeclared(node, key, Object.keys(props), declared)
|
|
692
693
|
const seed = pickDeclared(untracked(resolveProps), declared)
|
|
693
694
|
// mounting into a fragment attaches no shadow root of its own: a
|
|
694
695
|
// shadow-rendered child keeps its <style> elements inline, next to the DOM
|
|
@@ -819,6 +820,41 @@ const normalizeAllowUrl = (policy: any): AllowUrl => {
|
|
|
819
820
|
return () => false
|
|
820
821
|
}
|
|
821
822
|
|
|
823
|
+
// HTML's boolean attributes, verbatim from the spec's list. Presence is the
|
|
824
|
+
// whole message for these: `disabled="false"` and `disabled="0"` both disable,
|
|
825
|
+
// so the value they carry is noise. This is a table of a fact, not of a jq79
|
|
826
|
+
// convention - nobody in this repo decides what belongs in it, which is what
|
|
827
|
+
// earns it a place in a codebase that otherwise has no name tables
|
|
828
|
+
const BOOLEAN_ATTRS = new Set([
|
|
829
|
+
"allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls",
|
|
830
|
+
"default", "defer", "disabled", "formnovalidate", "inert", "ismap",
|
|
831
|
+
"itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open",
|
|
832
|
+
"playsinline", "readonly", "required", "reversed", "selected",
|
|
833
|
+
])
|
|
834
|
+
|
|
835
|
+
// the one value rule, shared by `:attr="expr"` and `:attrs` so the two forms
|
|
836
|
+
// can never disagree:
|
|
837
|
+
//
|
|
838
|
+
// - a boolean attribute is removed by ANY falsy value and set to "" when
|
|
839
|
+
// truthy, so `:disabled="items.length"` enables the button on an empty list
|
|
840
|
+
// (with `value !== false` as the only test, 0 set the attribute and disabled
|
|
841
|
+
// it - the trap renderComponent.test.ts used to pin);
|
|
842
|
+
// - every other attribute is removed only by null/undefined, so `false`, `0`
|
|
843
|
+
// and `""` are written. `aria-expanded="false"` and a `data-` flag mean
|
|
844
|
+
// something that absent cannot say.
|
|
845
|
+
//
|
|
846
|
+
// Asking the DOM which family a name belongs to (`typeof el[name] ===
|
|
847
|
+
// "boolean"`) is deliberately not what this does: jsdom and Chrome disagree on
|
|
848
|
+
// `autofocus` and every `aria-*`, so the tests would pin a semantics the
|
|
849
|
+
// browser doesn't have - and `readonly`/`novalidate`/`ismap` reflect under
|
|
850
|
+
// camelCase property names no kebab->camel pass can produce, failing toward
|
|
851
|
+
// `readonly="false"`, which is read-only
|
|
852
|
+
const applyAttr = (el: Element, name: string, value: any) => {
|
|
853
|
+
const boolean = BOOLEAN_ATTRS.has(name)
|
|
854
|
+
if (boolean ? !value : value == null) el.removeAttribute(name)
|
|
855
|
+
else el.setAttribute(name, boolean ? "" : String(value))
|
|
856
|
+
}
|
|
857
|
+
|
|
822
858
|
// renders a single element node: static attrs, @event listeners, a reactive
|
|
823
859
|
// :attrs object, and its content - :text/:html override the element's own
|
|
824
860
|
// children with a reactive textContent/innerHTML, otherwise children render
|
|
@@ -848,7 +884,11 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
848
884
|
// imported component after `await`). Watch for the key: the effect tracks
|
|
849
885
|
// no deps, so it only re-runs on the store's new-key sweep, and swaps the
|
|
850
886
|
// placeholder element for the component exactly once
|
|
851
|
-
|
|
887
|
+
// dashes included, because findComponentKey matches them case-insensitively
|
|
888
|
+
// with dashes stripped: <drop-area> resolves DropArea, so a dashed tag is a
|
|
889
|
+
// possible component too, not only a custom element
|
|
890
|
+
const mayUpgrade = el instanceof HTMLUnknownElement || node.tag.includes("-")
|
|
891
|
+
if (mayUpgrade) {
|
|
852
892
|
let upgraded = false
|
|
853
893
|
fx.effect(() => {
|
|
854
894
|
if (upgraded) return
|
|
@@ -871,10 +911,30 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
871
911
|
// the native-element form is parked there). Warn on a real element, but
|
|
872
912
|
// not on a tag that may still upgrade into a component - the upgrade
|
|
873
913
|
// re-renders through renderNestedComponent, models and all
|
|
874
|
-
if (!
|
|
914
|
+
if (!mayUpgrade) {
|
|
875
915
|
console.warn(`jq79: ${key} on <${node.tag}> does nothing - :model binds component tags only (for now)`)
|
|
876
916
|
}
|
|
877
|
-
} else if (
|
|
917
|
+
} else if (isControlAttr(key)) {
|
|
918
|
+
// a directive of its own, bound further down (or by renderNodes)
|
|
919
|
+
} else if (key.startsWith(":")) {
|
|
920
|
+
// :name="expr" binds that one attribute, reactively - the single-key
|
|
921
|
+
// case :attrs="{ name: expr }" was carrying. `:name` alone is shorthand
|
|
922
|
+
// for `:name="name"`, like props and :model.<name>, and the shorthand
|
|
923
|
+
// reads the camelCase variable while the attribute keeps its written
|
|
924
|
+
// (kebab) name: `:aria-expanded` binds `ariaExpanded`, because
|
|
925
|
+
// `aria-expanded` as an expression is a subtraction.
|
|
926
|
+
//
|
|
927
|
+
// On a tag that may still upgrade this is a *parameter*, not an
|
|
928
|
+
// attribute: leave it written verbatim, as before, so the upgrade's
|
|
929
|
+
// renderNestedComponent still finds it. A component tag has no single
|
|
930
|
+
// root for an attribute to land on anyway (TODOS/2026-07-15.class-directive.md)
|
|
931
|
+
if (mayUpgrade) el.setAttribute(key, value)
|
|
932
|
+
else {
|
|
933
|
+
const name = key.slice(1)
|
|
934
|
+
const expr = value || kebabToCamel(name)
|
|
935
|
+
fx.effect(() => applyAttr(el, name, evalExpr(expr, scope)))
|
|
936
|
+
}
|
|
937
|
+
} else el.setAttribute(key, value)
|
|
878
938
|
})
|
|
879
939
|
|
|
880
940
|
const bindExpr = node.attrs[":attrs"]
|
|
@@ -885,10 +945,7 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
|
|
|
885
945
|
boundKeys.forEach(key => el.removeAttribute(key))
|
|
886
946
|
const bound = evalExpr(bindExpr, scope)
|
|
887
947
|
boundKeys = bound && typeof bound === "object" ? Object.keys(bound) : []
|
|
888
|
-
boundKeys.forEach(key =>
|
|
889
|
-
const value = bound[key]
|
|
890
|
-
if (value != null && value !== false) el.setAttribute(key, String(value))
|
|
891
|
-
})
|
|
948
|
+
boundKeys.forEach(key => applyAttr(el, key, bound[key]))
|
|
892
949
|
})
|
|
893
950
|
}
|
|
894
951
|
|
|
@@ -1646,6 +1703,22 @@ const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
|
|
|
1646
1703
|
})
|
|
1647
1704
|
}
|
|
1648
1705
|
|
|
1706
|
+
// a setup script's signature. A bare `<script :setup>` is a CLOSED signature -
|
|
1707
|
+
// the same as `<script :setup="{}">`, declaring zero props and taking none -
|
|
1708
|
+
// because the difference between "takes nothing" and "takes anything" should
|
|
1709
|
+
// not be a pair of braces somebody didn't type. Permissive is still reachable,
|
|
1710
|
+
// it just has to be asked for: `<script :setup="_">`, the same `_` convention
|
|
1711
|
+
// factory scripts already use, which parsePropsPattern reads as no signature.
|
|
1712
|
+
//
|
|
1713
|
+
// Only the empty *value* is closed. An absent attribute (a factory <script>
|
|
1714
|
+
// with no :setup at all) stays `null`, so its signature is still read from the
|
|
1715
|
+
// factory's first parameter
|
|
1716
|
+
const setupSignature = (script: TagBlock): PropDecl[] | null => {
|
|
1717
|
+
const pattern = script.attrs[":setup"]
|
|
1718
|
+
if (pattern === undefined) return null
|
|
1719
|
+
return pattern.trim() === "" ? [] : parsePropsPattern(pattern)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1649
1722
|
// every prop name a component's scripts declare, across both script modes.
|
|
1650
1723
|
// Read before the store exists, because what a component declares decides
|
|
1651
1724
|
// which of its file's sibling components it can still see: declaring a name
|
|
@@ -1654,7 +1727,7 @@ const declareProps = (store: Record<string, any>, props: PropDecl[] | null) => {
|
|
|
1654
1727
|
const declaredPropNames = (scripts: TagBlock[]): Set<string> => {
|
|
1655
1728
|
const names = new Set<string>()
|
|
1656
1729
|
scripts.forEach(script => {
|
|
1657
|
-
const declarations = parseFactoryProps(script.content) ??
|
|
1730
|
+
const declarations = parseFactoryProps(script.content) ?? setupSignature(script)
|
|
1658
1731
|
declarations?.forEach(({ name }) => names.add(name))
|
|
1659
1732
|
})
|
|
1660
1733
|
return names
|
|
@@ -1662,13 +1735,13 @@ const declaredPropNames = (scripts: TagBlock[]): Set<string> => {
|
|
|
1662
1735
|
|
|
1663
1736
|
// the same names, but null when NO script declared a signature at all - the
|
|
1664
1737
|
// distinction declareProps already keeps, and the only one that can decide
|
|
1665
|
-
// whether to filter what a parent passes
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1738
|
+
// whether to filter what a parent passes. `<script :setup>` and
|
|
1739
|
+
// `<script :setup="{}">` are both closed signatures that take nothing (see
|
|
1740
|
+
// setupSignature); `<script :setup="_">` is the permissive one
|
|
1668
1741
|
const declaredPropSet = (scripts: TagBlock[]): Set<string> | null => {
|
|
1669
1742
|
let names: Set<string> | null = null
|
|
1670
1743
|
scripts.forEach(script => {
|
|
1671
|
-
const declarations = parseFactoryProps(script.content) ??
|
|
1744
|
+
const declarations = parseFactoryProps(script.content) ?? setupSignature(script)
|
|
1672
1745
|
if (!declarations) return
|
|
1673
1746
|
const into = (names ??= new Set())
|
|
1674
1747
|
declarations.forEach(({ name }) => into.add(name))
|
|
@@ -1690,6 +1763,30 @@ const pickDeclared = (props: Record<string, any>, declared: Set<string> | null):
|
|
|
1690
1763
|
return out
|
|
1691
1764
|
}
|
|
1692
1765
|
|
|
1766
|
+
// names already reported by warnUndeclared, keyed by the template node - which
|
|
1767
|
+
// is the usage site itself, built once and shared by every instance it ever
|
|
1768
|
+
// renders. So a :each over 200 rows says it once, not once per row, and a
|
|
1769
|
+
// definition swap doesn't repeat what the last one already said
|
|
1770
|
+
const undeclaredWarned = new WeakMap<TemplateNode, Set<string>>()
|
|
1771
|
+
|
|
1772
|
+
// a parameter the child's signature doesn't declare is dropped by pickDeclared
|
|
1773
|
+
// and never reaches its store - `{{ bar }}` renders empty at the other end of
|
|
1774
|
+
// the file. Written parameters only: this is handed the named ones (`:bar`,
|
|
1775
|
+
// and the prop each :model binds), never a `:props` spread's keys, because a
|
|
1776
|
+
// spread of an object wider than the component is the documented, intended use
|
|
1777
|
+
// and taking only the declared few is its point - see pickDeclared. A
|
|
1778
|
+
// component with no signature at all declares nothing to compare against
|
|
1779
|
+
const warnUndeclared = (node: TemplateNode, name: string, written: string[], declared: Set<string> | null) => {
|
|
1780
|
+
if (declared === null) return
|
|
1781
|
+
const said = undeclaredWarned.get(node) ?? new Set<string>()
|
|
1782
|
+
undeclaredWarned.set(node, said)
|
|
1783
|
+
written.forEach(prop => {
|
|
1784
|
+
if (declared.has(prop) || said.has(prop)) return
|
|
1785
|
+
said.add(prop)
|
|
1786
|
+
console.warn(`jq79: :${prop} is not declared by <${name}> - add it to the :setup signature, or drop it`)
|
|
1787
|
+
})
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1693
1790
|
// the sibling components this one resolves by name, or null when there are
|
|
1694
1791
|
// none left to resolve. They go on the store's *prototype* rather than in it:
|
|
1695
1792
|
// the component-key scan walks the chain, so <Row> resolves; they stay out of
|
|
@@ -2217,7 +2314,7 @@ export class Component79 {
|
|
|
2217
2314
|
return
|
|
2218
2315
|
}
|
|
2219
2316
|
const { vars, code } = transformSetupScript(script.content)
|
|
2220
|
-
declareProps(store,
|
|
2317
|
+
declareProps(store, setupSignature(script))
|
|
2221
2318
|
// pre-declare script vars on the store so `with` resolves assignments
|
|
2222
2319
|
// to them (and reads of them) through the reactive proxy
|
|
2223
2320
|
vars.forEach(name => { if (!(name in store)) (store as any)[name] = undefined })
|