@kudzujs/core 0.7.21 → 0.7.22
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 +2 -1
- package/RELEASES.md +28 -0
- package/framework/README.md +4 -2
- package/framework/binding-runtime.js +12 -4
- package/framework/build.mjs +86 -7
- package/framework/core.mjs +12 -6
- package/framework/list-runtime.js +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.7.
|
|
13
|
+
**Latest release: 0.7.22 - SVG structures and native links.** Reactive SVG conditionals and flat keyed lists preserve their namespace, while supported React Router `Link` authoring erases to base-aware native anchors with zero router runtime. Read the [release notes](./RELEASES.md#0722---svg-structures-and-native-links) or open the [release page](https://kudzujs.cloud/releases/0.7.22).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -82,6 +82,7 @@ ordinary React-shaped TSX
|
|
|
82
82
|
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities. Inline or simple `const` setter callbacks and object refs may cross one ordinary component boundary into a direct intrinsic root.
|
|
83
83
|
- Build-known data and routes become complete HTML through async components and `getStaticPaths()`.
|
|
84
84
|
- Native document navigation is the default; static routes do not load a client runtime.
|
|
85
|
+
- A named or aliased React Router `Link` with a static root-relative `to` erases to a base-aware native anchor; no router package or runtime is emitted.
|
|
85
86
|
- Unsupported nearby patterns fail during the build with a source location and actionable boundary.
|
|
86
87
|
|
|
87
88
|
Migration input may retain supported imports from `react`; Kudzu erases those references and never emits or executes React. New Kudzu source should import framework APIs from `@kudzujs/core`.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.22 - SVG structures and native links
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.22 extends ordinary React-shaped structural authoring into SVG and erases the common React Router `Link` form to native navigation without adding an SVG renderer or router runtime.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.22
|
|
8
|
+
|
|
9
|
+
- Reactive conditional branches inside SVG parse replacement markup in the actual SVG parent context and retain existing DOM ownership semantics.
|
|
10
|
+
- Flat intrinsic keyed lists inside SVG support add, update, reorder, and removal while preserving keyed identity and the SVG namespace.
|
|
11
|
+
- SVG fragment construction is compiled out of HTML-only condition and list builds; the measured matched fixture added 333 B raw / 79 B aggregate gzip JavaScript.
|
|
12
|
+
- A named or aliased `Link` import from `react-router-dom` with one static root-relative `to` lowers to a native `<a href>`, receives the configured `base`, and erases the package import.
|
|
13
|
+
- Dynamic or relative Link destinations, traversal, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics.
|
|
14
|
+
- Effect dependency arrays are explicitly proven for multiple direct primitive states or supported props through existing commit batching and `Object.is` comparison.
|
|
15
|
+
- The complete suite passes 111/111 tests with Chrome coverage for SVG namespace, conditional replacement, keyed identity, Link erasure, and zero-JavaScript static output.
|
|
16
|
+
|
|
17
|
+
### Measured fixture
|
|
18
|
+
|
|
19
|
+
On the recorded Intel i5-9500 / Chrome 142 environment, 1,000-row SVG medians were 0.8 ms conditional, 1.9 ms update, 8.3 ms reverse, 2.4 ms remove, and 3.5 ms add. The matched HTML control measured 0.7, 1.8, 8.3, 2.5, and 3.6 ms. Link and native-anchor controls emitted byte-identical 248 B HTML and zero JavaScript. Full methodology and raw arrays are in `PERFORMANCE.md`.
|
|
20
|
+
|
|
21
|
+
### Boundary
|
|
22
|
+
|
|
23
|
+
Structural SVG currently supports reactive conditionals and flat intrinsic keyed lists. Keyed-item conditions, nested SVG lists, reactive MathML, and namespaced attributes remain unsupported. React Router lowering accepts only direct named or aliased `Link` JSX with one safe static root-relative destination and native anchor props. Native document navigation remains the default; no SPA router is included.
|
|
24
|
+
|
|
25
|
+
### Upgrade
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @kudzujs/core@^0.7.22
|
|
29
|
+
```
|
|
30
|
+
|
|
3
31
|
## 0.7.21 - Composable collections and effects
|
|
4
32
|
|
|
5
33
|
Kudzu 0.7.21 expands ordinary React-shaped collection pipelines and effect authoring while preserving keyed identity, derived dependency semantics, and capability-specific browser output.
|
package/framework/README.md
CHANGED
|
@@ -4,9 +4,11 @@ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal so
|
|
|
4
4
|
|
|
5
5
|
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
|
+
A named or aliased `Link` import from `react-router-dom` may render directly with one static root-relative `to` plus native anchor props. The compiler prefixes the configured `base`, changes the element to `<a href>`, and erases the import, so native navigation remains the default and configured navigation groups see an ordinary eligible anchor. Dynamic or relative destinations, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics. No React Router package code or router runtime is emitted.
|
|
8
|
+
|
|
7
9
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
8
10
|
|
|
9
|
-
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. A top-level immutable local derived through a supported pure primitive expression from direct state may be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
11
|
+
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
10
12
|
|
|
11
13
|
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
|
|
12
14
|
|
|
@@ -31,7 +33,7 @@ Exact relative `.worker.ts` constructors in inline effects are validated and bun
|
|
|
31
33
|
|
|
32
34
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
33
35
|
|
|
34
|
-
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases
|
|
36
|
+
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases use the existing generic `setAttribute` path. Reactive conditionals and flat intrinsic keyed lists store inert branch or row markup on SVG markers and parse it in the actual parent namespace only when replacement nodes are needed; existing condition/list ownership then handles insertion, identity, updates, and removal. Builds without structural SVG compile out that fragment path, and static SVG adds no JavaScript. Keyed-item conditions, nested SVG lists, MathML structures, and namespaced attributes remain unsupported.
|
|
35
37
|
|
|
36
38
|
Same-file, directly exported same-file, and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. A block-bodied keyed `map` callback may declare one top-level `const` computed from a direct child collection through the supported pure selector pipeline and then return JSX; the alias must feed exactly one nested keyed list source. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
37
39
|
|
|
@@ -122,10 +122,10 @@ function mountConditions(root) {
|
|
|
122
122
|
mountedConditions.add(start)
|
|
123
123
|
const descriptor = JSON.parse(start.dataset.kIf)
|
|
124
124
|
const end = findEnd(start, descriptor.id)
|
|
125
|
-
const truthy = start.content.querySelector("template[data-k-true]")
|
|
126
|
-
const falsy = start.content.querySelector("template[data-k-false]")
|
|
125
|
+
const truthy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgTrue : start.content.querySelector("template[data-k-true]")
|
|
126
|
+
const falsy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgFalse : start.content.querySelector("template[data-k-false]")
|
|
127
127
|
if (!end || !truthy || !falsy) continue
|
|
128
|
-
const condition = { start, end, truthy, falsy, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: descriptor.owned }
|
|
128
|
+
const condition = { start, end, truthy, falsy, svg: globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: descriptor.owned }
|
|
129
129
|
mountConditionStates(condition, Boolean(descriptor.initial), false)
|
|
130
130
|
const mount = evaluator => {
|
|
131
131
|
if (!start.isConnected) return
|
|
@@ -154,7 +154,9 @@ function updateCondition(condition) {
|
|
|
154
154
|
const falseText = condition.kind === "and" && !truthy ? renderFalsy(value) : ""
|
|
155
155
|
const fragment = falseText
|
|
156
156
|
? textFragment(condition.end.ownerDocument, falseText)
|
|
157
|
-
:
|
|
157
|
+
: globalThis.__KUDZU_SVG_CONDITIONS__ && condition.svg
|
|
158
|
+
? svgFragment(condition.start, truthy ? condition.truthy : condition.falsy)
|
|
159
|
+
: (truthy ? condition.truthy : condition.falsy).content.cloneNode(true)
|
|
158
160
|
const nodes = condition.mount ? [...fragment.childNodes] : undefined
|
|
159
161
|
mountConditionStates(condition, truthy, true)
|
|
160
162
|
condition.end.parentNode.insertBefore(fragment, condition.end)
|
|
@@ -230,6 +232,12 @@ function textFragment(document, value) {
|
|
|
230
232
|
return fragment
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
function svgFragment(marker, markup) {
|
|
236
|
+
const range = marker.ownerDocument.createRange()
|
|
237
|
+
range.selectNode(marker)
|
|
238
|
+
return range.createContextualFragment(markup)
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
function findEnd(start, id) {
|
|
234
242
|
for (let node = start.nextSibling; node; node = node.nextSibling) {
|
|
235
243
|
if (node.nodeType === Node.ELEMENT_NODE && node.matches("template[data-k-if-end]") && node.dataset.kIfEnd === id) return node
|
package/framework/build.mjs
CHANGED
|
@@ -199,7 +199,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
199
199
|
const commandEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.commands).map(event => event.event)))].sort()
|
|
200
200
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
201
201
|
const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
|
|
202
|
+
const hasSvgConditions = plans.some(plan => plan.conditions.some(condition => condition.svg))
|
|
202
203
|
const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
|
|
204
|
+
const hasSvgLists = plans.some(plan => plan.lists.some(list => list.svg))
|
|
203
205
|
const hasDeepListConditions = plans.some(plan => plan.lists.some(list => list.conditionHandlers))
|
|
204
206
|
const hasListTextRanges = plans.some(plan => plan.lists.some(list => list.textRanges))
|
|
205
207
|
const hasListAttributes = plans.some(plan => plan.lists.some(list => list.attributes))
|
|
@@ -272,6 +274,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
272
274
|
if (navigationRoutes.length) bindingRuntime = specializeNavigationTextDescriptors(bindingRuntime)
|
|
273
275
|
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
274
276
|
"globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
|
|
277
|
+
"globalThis.__KUDZU_SVG_CONDITIONS__": String(hasSvgConditions),
|
|
275
278
|
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
|
|
276
279
|
})
|
|
277
280
|
}
|
|
@@ -349,7 +352,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
349
352
|
__KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
|
|
350
353
|
__KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
|
|
351
354
|
__KUDZU_LIST_INDEXES__: String(hasListIndexes),
|
|
352
|
-
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths)
|
|
355
|
+
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths),
|
|
356
|
+
__KUDZU_SVG_LISTS__: String(hasSvgLists)
|
|
353
357
|
})
|
|
354
358
|
if (hasCollectionSelectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
|
|
355
359
|
}
|
|
@@ -1734,7 +1738,8 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
1734
1738
|
if (errors.length) {
|
|
1735
1739
|
throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
1736
1740
|
}
|
|
1737
|
-
|
|
1741
|
+
const packageReference = emittedPackageReference(result.outputText, file, new Set(["react", "react-router-dom"]))
|
|
1742
|
+
if (packageReference) throw new Error(`${relative(root, file)} Runtime ${packageReference} module references are not supported`)
|
|
1738
1743
|
|
|
1739
1744
|
const output = compiledPath(file)
|
|
1740
1745
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
@@ -1757,18 +1762,88 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
1757
1762
|
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
|
|
1758
1763
|
}
|
|
1759
1764
|
|
|
1760
|
-
function
|
|
1765
|
+
function emittedPackageReference(source, file, packages) {
|
|
1761
1766
|
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
|
|
1762
|
-
let found
|
|
1767
|
+
let found
|
|
1763
1768
|
const visit = node => {
|
|
1764
|
-
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text
|
|
1765
|
-
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text
|
|
1769
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && packages.has(node.moduleSpecifier.text)) found = node.moduleSpecifier.text
|
|
1770
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && packages.has(node.arguments[0].text)) found = node.arguments[0].text
|
|
1766
1771
|
if (!found) ts.forEachChild(node, visit)
|
|
1767
1772
|
}
|
|
1768
1773
|
visit(sourceFile)
|
|
1769
1774
|
return found
|
|
1770
1775
|
}
|
|
1771
1776
|
|
|
1777
|
+
function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
|
|
1778
|
+
const links = new Set()
|
|
1779
|
+
for (const statement of sourceFile.statements) {
|
|
1780
|
+
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1781
|
+
if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
|
|
1782
|
+
const clause = statement.importClause
|
|
1783
|
+
if (clause?.isTypeOnly) continue
|
|
1784
|
+
if (!clause) throw sourceNodeError(statement, sourceFile, "Side-effect React Router imports are not supported")
|
|
1785
|
+
if (clause.name) throw sourceNodeError(clause.name, sourceFile, "React Router default imports are not supported; use a named Link import")
|
|
1786
|
+
const bindings = clause.namedBindings
|
|
1787
|
+
if (!bindings || ts.isNamespaceImport(bindings)) throw sourceNodeError(bindings ?? statement, sourceFile, "React Router namespace imports are not supported; use a named Link import")
|
|
1788
|
+
for (const entry of bindings.elements) {
|
|
1789
|
+
if (entry.isTypeOnly) continue
|
|
1790
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
1791
|
+
if (imported === "NavLink") throw sourceNodeError(entry, sourceFile, "React Router NavLink active-route semantics cannot be erased to a native anchor")
|
|
1792
|
+
if (imported !== "Link") throw sourceNodeError(entry, sourceFile, `React Router ${imported} is not supported; only named Link imports can be erased to native anchors`)
|
|
1793
|
+
links.add(entry.name.text)
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (!links.size) return sourceFile
|
|
1798
|
+
|
|
1799
|
+
const routerProps = new Set(["discover", "end", "prefetch", "preventScrollReset", "relative", "reloadDocument", "replace", "state", "viewTransition"])
|
|
1800
|
+
const attributes = attributesNode => {
|
|
1801
|
+
const output = []
|
|
1802
|
+
let destination
|
|
1803
|
+
for (const property of attributesNode.properties) {
|
|
1804
|
+
if (ts.isJsxSpreadAttribute(property)) throw sourceNodeError(property, sourceFile, "React Router Link does not support spread attributes during native anchor lowering")
|
|
1805
|
+
const name = property.name.text
|
|
1806
|
+
if (name === "href") throw sourceNodeError(property, sourceFile, "React Router Link must not declare href; Kudzu derives it from to")
|
|
1807
|
+
if (routerProps.has(name)) throw sourceNodeError(property, sourceFile, `React Router Link prop ${JSON.stringify(name)} cannot be erased to a native anchor`)
|
|
1808
|
+
if (name !== "to") {
|
|
1809
|
+
output.push(ts.visitEachChild(property, visitor, context))
|
|
1810
|
+
continue
|
|
1811
|
+
}
|
|
1812
|
+
if (destination !== undefined) throw sourceNodeError(property, sourceFile, "React Router Link requires exactly one to attribute")
|
|
1813
|
+
if (!property.initializer || !ts.isStringLiteral(property.initializer)) throw sourceNodeError(property, sourceFile, 'React Router Link requires a static root-relative to="/path"')
|
|
1814
|
+
destination = property.initializer.text
|
|
1815
|
+
const pathname = destination.match(/^[^?#]*/)[0]
|
|
1816
|
+
let decoded
|
|
1817
|
+
try { decoded = decodeURIComponent(pathname) } catch { throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"') }
|
|
1818
|
+
if (!destination.startsWith("/") || destination.startsWith("//") || /%(?:2f|5c)/i.test(pathname) || /[\\\0]/.test(decoded) || decoded.split("/").includes("..") || [...decoded].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159)) throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"')
|
|
1819
|
+
output.push(factory.createJsxAttribute(factory.createIdentifier("href"), factory.createStringLiteral(withBase(base, destination))))
|
|
1820
|
+
}
|
|
1821
|
+
if (destination === undefined) throw sourceNodeError(attributesNode.parent, sourceFile, "React Router Link requires exactly one static root-relative to attribute")
|
|
1822
|
+
return factory.updateJsxAttributes(attributesNode, output)
|
|
1823
|
+
}
|
|
1824
|
+
const importedLink = tag => ts.isIdentifier(tag) && links.has(tag.text) && !isShadowedIdentifier(tag, sourceFile)
|
|
1825
|
+
const visitor = node => {
|
|
1826
|
+
if (ts.isJsxElement(node) && importedLink(node.openingElement.tagName)) {
|
|
1827
|
+
const opening = factory.updateJsxOpeningElement(node.openingElement, factory.createIdentifier("a"), node.openingElement.typeArguments, attributes(node.openingElement.attributes))
|
|
1828
|
+
const closing = factory.updateJsxClosingElement(node.closingElement, factory.createIdentifier("a"))
|
|
1829
|
+
return factory.updateJsxElement(node, opening, ts.visitNodes(node.children, visitor), closing)
|
|
1830
|
+
}
|
|
1831
|
+
if (ts.isJsxSelfClosingElement(node) && importedLink(node.tagName)) return factory.updateJsxSelfClosingElement(node, factory.createIdentifier("a"), node.typeArguments, attributes(node.attributes))
|
|
1832
|
+
if (ts.isIdentifier(node) && links.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router Link imports may only be used as direct JSX elements")
|
|
1833
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react-router-dom") {
|
|
1834
|
+
const clause = node.importClause
|
|
1835
|
+
if (!clause || clause.isTypeOnly) return node
|
|
1836
|
+
const bindings = clause.namedBindings
|
|
1837
|
+
if (!bindings || !ts.isNamedImports(bindings)) return node
|
|
1838
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "Link")
|
|
1839
|
+
if (!elements.length) return undefined
|
|
1840
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, undefined, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
|
|
1841
|
+
}
|
|
1842
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1843
|
+
}
|
|
1844
|
+
return ts.visitNode(sourceFile, visitor)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1772
1847
|
function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
1773
1848
|
const names = new Set()
|
|
1774
1849
|
for (const statement of sourceFile.statements) {
|
|
@@ -2298,6 +2373,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2298
2373
|
const factory = context.factory
|
|
2299
2374
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
2300
2375
|
const importedCollections = importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex)
|
|
2376
|
+
sourceFile = normalizeReactRouterSyntax(sourceFile, factory, context, base)
|
|
2377
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2301
2378
|
sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
|
|
2302
2379
|
ts.setParentRecursive(sourceFile, false)
|
|
2303
2380
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
@@ -2316,7 +2393,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2316
2393
|
const importedSource = target => {
|
|
2317
2394
|
let imported = importedSources.get(target)
|
|
2318
2395
|
if (!imported) {
|
|
2319
|
-
imported =
|
|
2396
|
+
imported = normalizeReactRouterSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context, base)
|
|
2397
|
+
ts.setParentRecursive(imported, false)
|
|
2398
|
+
imported = normalizeClsxSyntax(imported, factory, context)
|
|
2320
2399
|
ts.setParentRecursive(imported, false)
|
|
2321
2400
|
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2322
2401
|
ts.setParentRecursive(imported, false)
|
package/framework/core.mjs
CHANGED
|
@@ -588,7 +588,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
588
588
|
const descriptor = bindingDescriptor(node)
|
|
589
589
|
const stateIds = reactiveStateIds(descriptor)
|
|
590
590
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
591
|
-
if (namespace) throw new Error(
|
|
591
|
+
if (namespace === "math") throw new Error("Reactive conditional DOM is not supported inside math")
|
|
592
592
|
|
|
593
593
|
const id = renderContext.listRoot || renderContext.listRowRoot ? nextRowRenderId("c") : nextRenderId("c")
|
|
594
594
|
const renderBranch = async branch => {
|
|
@@ -609,14 +609,17 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
609
609
|
}
|
|
610
610
|
const owned = truthy.states.length || falsy.states.length ? { true: truthy.states, false: falsy.states } : undefined
|
|
611
611
|
const mount = Boolean(owned) || truthy.html.includes("data-k-") || falsy.html.includes("data-k-") || truthy.html.includes("<!--k-text:") || falsy.html.includes("<!--k-text:")
|
|
612
|
-
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(owned ? { owned } : {}), ...(mount ? { mount: true } : {}) }
|
|
612
|
+
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(namespace === "svg" ? { svg: true } : {}), ...(owned ? { owned } : {}), ...(mount ? { mount: true } : {}) }
|
|
613
613
|
for (const stateId of stateIds) renderContext.conditionStates.add(stateId)
|
|
614
614
|
renderContext.conditions.push(metadata)
|
|
615
615
|
renderContext.hasBehaviors = true
|
|
616
616
|
renderContext.hasBindings = true
|
|
617
617
|
const encoded = escapeJsonAttribute(metadata)
|
|
618
618
|
const current = node.value ? truthy.html : node.kind === "and" ? await renderNode(node.value, namespace, selectValue) : falsy.html
|
|
619
|
-
|
|
619
|
+
const branches = namespace === "svg"
|
|
620
|
+
? ` data-k-svg-true="${escapeAttribute(truthy.html)}" data-k-svg-false="${escapeAttribute(falsy.html)}"></template>`
|
|
621
|
+
: `><template data-k-true>${truthy.html}</template><template data-k-false>${falsy.html}</template></template>`
|
|
622
|
+
return `<template data-k-if='${encoded}'${branches}${current}<template data-k-if-end="${id}"></template>`
|
|
620
623
|
}
|
|
621
624
|
if (node?.[listMarker]) return renderList(node, namespace, selectValue)
|
|
622
625
|
if (node?.[listFieldMarker]) {
|
|
@@ -644,6 +647,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
644
647
|
return `<!--k-text:${id}-->${escapeHtml(node.value ?? "")}<!--k-text-end-->`
|
|
645
648
|
}
|
|
646
649
|
if (node?.[listConditionalMarker]) {
|
|
650
|
+
if (namespace === "svg") throw new Error("Keyed row conditions are not supported inside svg")
|
|
647
651
|
const descriptor = { kind: node.kind, module: node.module, handler: node.handler }
|
|
648
652
|
const owner = renderContext.listRoot ?? renderContext.listRowRoot
|
|
649
653
|
if (owner) owner.conditions = true
|
|
@@ -833,12 +837,13 @@ function sharedInitialListMarker() {
|
|
|
833
837
|
}
|
|
834
838
|
|
|
835
839
|
async function renderList(node, namespace, selectValue) {
|
|
836
|
-
if (namespace) throw new Error(
|
|
840
|
+
if (namespace === "math") throw new Error("Reactive keyed lists are not supported inside math")
|
|
841
|
+
if (namespace === "svg" && node.ownerField) throw new Error("Nested reactive keyed lists are not supported inside svg")
|
|
837
842
|
const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
|
|
838
843
|
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
839
844
|
const rowList = node.ownerField ? nextRowList() : undefined
|
|
840
845
|
const id = rowList?.id ?? nextRenderId("l")
|
|
841
|
-
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(node.items[internalStateMarker] ? { static: true } : {}), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.selectorStates.length ? { selectorStates: Object.fromEntries(node.selectorStates.map(([name, state]) => [name, state.id])) } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
846
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(namespace === "svg" ? { svg: true } : {}), ...(node.items[internalStateMarker] ? { static: true } : {}), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.selectorStates.length ? { selectorStates: Object.fromEntries(node.selectorStates.map(([name, state]) => [name, state.id])) } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
842
847
|
if (ownerTemplate) {
|
|
843
848
|
ownerRoot.descriptor.children ??= []
|
|
844
849
|
ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
|
|
@@ -904,7 +909,8 @@ async function renderList(node, namespace, selectValue) {
|
|
|
904
909
|
renderContext.hasBehaviors = true
|
|
905
910
|
renderContext.hasLists = true
|
|
906
911
|
const prototype = node.ownerField && !ownerTemplate ? "" : template
|
|
907
|
-
|
|
912
|
+
const svgTemplate = namespace === "svg" ? ` data-k-svg-template="${escapeAttribute(prototype)}"` : ""
|
|
913
|
+
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'${svgTemplate}>${namespace === "svg" ? "" : prototype}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
908
914
|
} finally {
|
|
909
915
|
renderContext.listRoot = previousListRoot
|
|
910
916
|
renderContext.listRowRoot = previousListRowRoot
|
|
@@ -44,7 +44,7 @@ function mountLists(root) {
|
|
|
44
44
|
const end = findEnd(start, descriptor.id)
|
|
45
45
|
const roots = listRoots(start, end)
|
|
46
46
|
const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
|
|
47
|
-
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start
|
|
47
|
+
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : listTemplateRoot(start, descriptor)
|
|
48
48
|
if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
|
|
49
49
|
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
50
50
|
const staticRows = __KUDZU_STATIC_COLLECTIONS__ && descriptor.static && parts.directFill ? new Map() : undefined
|
|
@@ -63,7 +63,8 @@ function mountLists(root) {
|
|
|
63
63
|
const list = {
|
|
64
64
|
start,
|
|
65
65
|
descriptor,
|
|
66
|
-
|
|
66
|
+
templateRoot,
|
|
67
|
+
...(__KUDZU_NESTED_LISTS__ && nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}),
|
|
67
68
|
parts,
|
|
68
69
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticRows ? { staticRows } : {}),
|
|
69
70
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticEntries ? { staticEntries, staticPositions: staticEntries.positions } : {}),
|
|
@@ -196,7 +197,7 @@ function updateList(list) {
|
|
|
196
197
|
let node = list.roots.get(token)
|
|
197
198
|
if (!node) {
|
|
198
199
|
const staticRoot = __KUDZU_STATIC_COLLECTIONS__ ? list.staticRows?.get(token)?.cloneNode(true) : undefined
|
|
199
|
-
node = staticRoot ??
|
|
200
|
+
node = staticRoot ?? list.templateRoot?.cloneNode(true)
|
|
200
201
|
if (!staticRoot && node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
201
202
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
202
203
|
node.removeAttribute("data-k-list-root")
|
|
@@ -360,7 +361,7 @@ function updateStableList(list, items) {
|
|
|
360
361
|
keys.add(token)
|
|
361
362
|
if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index])) stable = false
|
|
362
363
|
if (!stable || items.length === previous.length + 1 || index < previous.length) continue
|
|
363
|
-
let node =
|
|
364
|
+
let node = list.templateRoot?.cloneNode(true)
|
|
364
365
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
365
366
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
366
367
|
node.removeAttribute("data-k-list-root")
|
|
@@ -545,7 +546,7 @@ function updateReducerList(list, items) {
|
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
function addListRoot(list, { item, index = list.roots.size, key, token, value }) {
|
|
548
|
-
let node =
|
|
549
|
+
let node = list.templateRoot?.cloneNode(true)
|
|
549
550
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
550
551
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
551
552
|
node.removeAttribute("data-k-list-root")
|
|
@@ -862,7 +863,7 @@ function listOwner(start) {
|
|
|
862
863
|
function mountNestedPrototype(start, descriptor, roots) {
|
|
863
864
|
const owner = descriptor.ownerField ? listOwner(start) : undefined
|
|
864
865
|
const prototypeStart = owner ? childPrototypes.get(owner)?.get(descriptor.id) : undefined
|
|
865
|
-
const templateRoot = prototypeStart?.content.firstElementChild ?? start
|
|
866
|
+
const templateRoot = prototypeStart?.content.firstElementChild ?? listTemplateRoot(start, descriptor)
|
|
866
867
|
if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
|
|
867
868
|
const prototypes = descriptor.children && new Map(descriptor.children.map(child => [child.id, findChildPrototype(templateRoot, child.id)]))
|
|
868
869
|
if (prototypes && [...prototypes.values()].some(prototype => !prototype)) throw new Error("Keyed list template has no nested row prototype")
|
|
@@ -871,6 +872,13 @@ function mountNestedPrototype(start, descriptor, roots) {
|
|
|
871
872
|
return { owner, childPrototypes: prototypes, templateRoot }
|
|
872
873
|
}
|
|
873
874
|
|
|
875
|
+
function listTemplateRoot(start, descriptor) {
|
|
876
|
+
if (!__KUDZU_SVG_LISTS__ || !descriptor.svg) return start.content.firstElementChild
|
|
877
|
+
const range = start.ownerDocument.createRange()
|
|
878
|
+
range.selectNode(start)
|
|
879
|
+
return range.createContextualFragment(start.dataset.kSvgTemplate).firstElementChild
|
|
880
|
+
}
|
|
881
|
+
|
|
874
882
|
function findChildPrototype(root, id) {
|
|
875
883
|
for (const element of root.children) {
|
|
876
884
|
if (element.matches("template[data-k-list]")) {
|